最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

JMM核心概念之Happens-before原則

 更新時間:2021年06月21日 11:01:32   作者:飛天小牛肉  
關于Java并發(fā)的通信機制是基于共享內存實現(xiàn)的,線程之間共享程序的公共狀態(tài),通過寫-讀內存中的公共狀態(tài)進行隱式通信,這對程序員是透明的,我們需要理解其工作機制,以防止內存可見性問題,從而編寫出正確同步的代碼

一、前言

關于 Happens-before,《Java 并發(fā)編程的藝術》書中是這樣介紹的:

Happens-before 是 JMM 最核心的概念。對應 Java 程序員來說,理解 Happens-before 是理解 JMM 的關鍵。

《深入理解 Java 虛擬機 - 第 3 版》書中是這樣介紹的:

Happens-before 是 JMM 的靈魂,它是判斷數(shù)據(jù)是否存在競爭,線程是否安全的非常有用的手段。

我想,這兩句話就已經(jīng)足夠表明 Happens-before 原則的重要性。

那為什么 Happens-before 被不約而同的稱為 JMM 的核心和靈魂呢?

二、JMM 設計者的難題與完美的解決方案

事實上,從 JMM 設計者的角度來看,可見性和有序性其實是互相矛盾的兩點:

  • 一方面,對于程序員來說,我們希望內存模型易于理解、易于編程,為此 JMM 的設計者要為程序員提供足夠強的內存可見性保證,專業(yè)術語稱之為 “強內存模型”。
  • 而另一方面,編譯器和處理器則希望內存模型對它們的束縛越少越好,這樣它們就可以做盡可能多的優(yōu)化(比如重排序)來提高性能,因此 JMM 的設計者對編譯器和處理器的限制要盡可能地放松,專業(yè)術語稱之為 “弱內存模型”。

對于這個問題,從 JDK 5 開始,也就是在 JSR-133 內存模型中,終于給出了一套完美的解決方案,那就是 Happens-before 原則,Happens-before 直譯為 “先行發(fā)生”,《JSR-133:Java Memory Model and Thread Specification》對 Happens-before 關系的定義如下:

1)如果一個操作 Happens-before 另一個操作,那么第一個操作的執(zhí)行結果將對第二個操作可見,而且第一個操作的執(zhí)行順序排在第二個操作之前。

2)兩個操作之間存在 Happens-before 關系,并不意味著 Java 平臺的具體實現(xiàn)必須要按照 Happens-before 關系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結果,與按 Happens-before 關系來執(zhí)行的結果一致,那么這種重排序并不非法(也就是說,JMM 允許這種重排序)

并不難理解,第 1 條定義是 JMM 對程序員強內存模型的承諾。從程序員的角度來說,可以這樣理解 Happens-before 關系:如果 A Happens-before B,那么 JMM 將向程序員保證 — A 操作的結果將對 B 可見,且 A 的執(zhí)行順序排在 B 之前。注意,這只是 Java內存模型向程序員做出的保證!

需要注意的是,不同于 as-if-serial 語義只能作用在單線程,這里提到的兩個操作 A 和 B 既可以是在一個線程之內,也可以是在不同線程之間。也就是說,Happens-before 提供跨線程的內存可見性保證。

針對這個第 1 條定義,我來舉個例子:

// 以下操作在線程 A 中執(zhí)行
i = 1; // a

// 以下操作在線程 B 中執(zhí)行
j = i; // b

// 以下操作在線程 C 中執(zhí)行
i = 2; // c

假設線程 A 中的操作 a Happens-before 線程 B 的操作 b,那我們就可以確定操作 b 執(zhí)行后,變量 j 的值一定是等于 1。

得出這個結論的依據(jù)有兩個:一是根據(jù) Happens-before 原則,a 操作的結果對 b 可見,即 “i=1” 的結果可以被觀察到;二是線程 C 還沒運行,線程 A 操作結束之后沒有其他線程會修改變量 i 的值。

現(xiàn)在再來考慮線程 C,我們依然保持 a Happens-before b ,而 c 出現(xiàn)在 a 和 b 的操作之間,但是 c 與 b 沒有 Happens-before 關系,也就是說 b 并不一定能看到 c 的操作結果。那么 b 操作的結果也就是 j 的值就不確定了,可能是 1 也可能是 2,那這段代碼就是線程不安全的。

再來看 Happens-before 的第 2 條定義,這是 JMM 對編譯器和處理器弱內存模型的保證,在給予充分的可操作空間下,對編譯器和處理器的重排序進行一定的約束。也就是說,JMM 其實是在遵循一個基本原則:只要不改變程序的執(zhí)行結果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。

JMM 這么做的原因是:程序員對于這兩個操作是否真的被重排序并不關心,程序員關心的是執(zhí)行結果不能被改變。

文字可能不是很好理解,我們舉個例子,來解釋下第 2 條定義:雖然兩個操作之間存在 Happens-before 關系,但不意味著 Java 平臺的具體實現(xiàn)必須要按照 Happens-before 關系指定的順序來執(zhí)行。

int a = 1; 		// A
int b = 2;		// B
int c = a + b;	// C

根據(jù) Happens-before 規(guī)則(下文會講),上述代碼存在 3 個 Happens-before 關系:

1)A Happens-before B

2)B Happens-before C

3)A Happens-before C

可以看出來,在 3 個 Happens-before 關系中,第 2 個和第 3 個是必需的,但第 1 個是不必要的。

也就是說,雖然 A Happens-before B,但是 A 和 B 之間的重排序完全不會改變程序的執(zhí)行結果,所以 JMM 是允許編譯器和處理器執(zhí)行這種重排序的。

看下面這張 JMM 的設計圖更直觀:

其實,可以這么簡單的理解,為了避免 Java 程序員為了理解 JMM 提供的內存可見性保證而去學習復雜的重排序規(guī)則以及這些規(guī)則的具體實現(xiàn)方法,JMM 就出了這么一個簡單易懂的 Happens-before 原則,一個 Happens-before 規(guī)則就對應于一個或多個編譯器和處理器的重排序規(guī)則,這樣,我們只需要弄明白 Happens-before 就行了。

三、8 條 Happens-before 規(guī)則

《JSR-133:Java Memory Model and Thread Specification》定義了如下 Happens-before 規(guī)則, 這些就是 JMM 中“天然的” Happens-before 關系,這些 Happens-before 關系無須任何同步器協(xié)助就已經(jīng)存在,可以在編碼中直接使用。如果兩個操作之間的關系不在此列,并且無法從下列規(guī)則推導出來,則它們就沒有順序性保障,JVM 可以對它們隨意地進行重排序:

1)程序次序規(guī)則(Program Order Rule):在一個線程內,按照控制流順序,書寫在前面的操作先行發(fā)生(Happens-before)于書寫在后面的操作。注意,這里說的是控制流順序而不是程序代碼順序,因為要考慮分支、循環(huán)等結構。

這個很好理解,符合我們的邏輯思維。比如我們上面舉的例子:

int a = 1; 		// A
int b = 2;		// B
int c = a + b;	// C

根據(jù)程序次序規(guī)則,上述代碼存在 3 個 Happens-before 關系:

  • A Happens-before B
  • B Happens-beforeC
  • A Happens-before C

2)管程鎖定規(guī)則(Monitor Lock Rule):一個 unlock 操作先行發(fā)生于后面對同一個鎖的 lock 操作。這里必須強調的是 “同一個鎖”,而 “后面” 是指時間上的先后。

這個規(guī)則其實就是針對 synchronized 的。JVM 并沒有把 lockunlock 操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令 monitorentermonitorexit 來隱式地使用這兩個操作。這兩個字節(jié)碼指令反映到 Java 代碼中就是同步塊 — synchronized

舉個例子:

synchronized (this) { // 此處自動加鎖
	if (x < 1) {
        x = 1;
    }      
} // 此處自動解鎖

根據(jù)管程鎖定規(guī)則,假設 x 的初始值是 10,線程 A 執(zhí)行完代碼塊后 x 的值會變成 1,執(zhí)行完自動釋放鎖,線程 B 進入代碼塊時,能夠看到線程 A 對 x 的寫操作,也就是線程 B 能夠看到 x == 1。

3)volatile 變量規(guī)則(Volatile Variable Rule):對一個 volatile 變量的寫操作先行發(fā)生于后面對這個變量的讀操作,這里的 “后面” 同樣是指時間上的先后。

這個規(guī)則就是 JDK 1.5 版本對 volatile 語義的增強,其意義之重大,靠著這個規(guī)則搞定可見性易如反掌。

舉個例子:

假設線程 A 執(zhí)行 writer() 方法之后,線程 B 執(zhí)行 reader() 方法。

根據(jù)根據(jù)程序次序規(guī)則:1 Happens-before 2;3 Happens-before 4。

根據(jù) volatile 變量規(guī)則:2 Happens-before 3。

根據(jù)傳遞性規(guī)則:1 Happens-before 3;1 Happens-before 4。

也就是說,如果線程 B 讀到了 “flag==true” 或者 “int i = a” 那么線程 A 設置的“a=42”對線程 B 是可見的。

看下圖:

4)線程啟動規(guī)則(Thread Start Rule):Thread 對象的 start() 方法先行發(fā)生于此線程的每一個動作。

比如說主線程 A 啟動子線程 B 后,子線程 B 能夠看到主線程在啟動子線程 B 前的所有操作。

5)線程終止規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對此線程的終止檢測,我們可以通過 Thread 對象的 join() 方法是否結束、Thread 對象的 isAlive() 的返回值等手段檢測線程是否已經(jīng)終止執(zhí)行。

6)線程中斷規(guī)則(Thread Interruption Rule):對線程 interrupt() 方法的調用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過 Thread 對象的 interrupted() 方法檢測到是否有中斷發(fā)生。

7)對象終結規(guī)則(Finalizer Rule):一個對象的初始化完成(構造函數(shù)執(zhí)行結束)先行發(fā)生于它的 finalize() 方法的開始。

8)傳遞性(Transitivity):如果操作 A 先行發(fā)生于操作 B,操作 B 先行發(fā)生于操作 C,那就可以得出操作 A 先行發(fā)生于操作 C 的結論。

四、“時間上的先發(fā)生” 與 “先行發(fā)生”

上述 8 種規(guī)則中,還不斷提到了時間上的先后,那么,“時間上的先發(fā)生” 與 “先行發(fā)生(Happens-before)” 到底有啥區(qū)別?

一個操作 “時間上的先發(fā)生” 是否就代表這個操作會是“先行發(fā)生” 呢?一個操作 “先行發(fā)生” 是否就能推導出這個操作必定是“時間上的先發(fā)生”呢?

很遺憾,這兩個推論都是不成立的。

舉兩個例子論證一下:

private int value = 0;

// 線程 A 調用
pubilc void setValue(int value){    
    this.value = value;
}

// 線程 B 調用
public int getValue(){
    return value;
}

假設存在線程 A 和 B,線程 A 先(時間上的先后)調用了 setValue(1),然后線程 B 調用了同一個對象的 getValue() ,那么線程 B 收到的返回值是什么?

我們根據(jù)上述 Happens-before 的 8 大規(guī)則依次分析一下:

由于兩個方法分別由線程 A 和 B 調用,不在同一個線程中,所以程序次序規(guī)則在這里不適用;

由于沒有 synchronized 同步塊,自然就不會發(fā)生 lock 和 unlock 操作,所以管程鎖定規(guī)則在這里不適用;

同樣的,volatile 變量規(guī)則,線程啟動、終止、中斷規(guī)則和對象終結規(guī)則也和這里完全沒有關系。

因為沒有一個適用的 Happens-before 規(guī)則,所以第 8 條規(guī)則傳遞性也無從談起。

因此我們可以判定,盡管線程 A 在操作時間上來看是先于線程 B 的,但是并不能說 A Happens-before B,也就是 A 線程操作的結果 B 不一定能看到。所以,這段代碼是線程不安全的。

想要修復這個問題也很簡單?既然不滿足 Happens-before 原則,那我修改下讓它滿足不就行了。比如說把 Getter/Setter 方法都用 synchronized 修飾,這樣就可以套用管程鎖定規(guī)則;再比如把 value 定義為 volatile 變量,這樣就可以套用 volatile 變量規(guī)則等。

這個例子,就論證了一個操作 “時間上的先發(fā)生” 不代表這個操作會是 “先行發(fā)生(Happens-before)”。

再來看一個例子:

// 以下操作在同一個線程中執(zhí)行
int i = 1;
int j = 2;

假設這段代碼中的兩條賦值語句在同一個線程之中,那么根據(jù)程序次序規(guī)則,“int i = 1” 的操作先行發(fā)生(Happens-before)于 “int j = 2”,但是,還記得 Happens-before 的第 2 條定義嗎?還記得上文說過 JMM 實際上是遵守這樣的一條原則:只要不改變程序的執(zhí)行結果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。

所以,“int j=2” 這句代碼完全可能優(yōu)先被處理器執(zhí)行,因為這并不影響程序的最終運行結果。

那么,這個例子,就論證了一個操作 “先行發(fā)生(Happens-before)” 不代表這個操作一定是“時間上的先發(fā)生”。

這樣,綜上兩例,我們可以得出這樣一個結論:Happens-before 原則與時間先后順序之間基本沒有因果關系,所以我們在衡量并發(fā)安全問題的時候,盡量不要受時間順序的干擾,一切必須以 Happens-before 原則為準。

五、Happens-before 與 as-if-serial

綜上,我覺得其實讀懂了下面這句話也就讀懂了 Happens-before 了,這句話上文也出現(xiàn)過幾次:JMM 其實是在遵循一個基本原則,即只要不改變程序的執(zhí)行結果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。

再回顧下 as-if-serial 語義:不管怎么重排序,單線程環(huán)境下程序的執(zhí)行結果不能被改變。

各位發(fā)現(xiàn)沒有?本質上來說 Happens-before 關系和 as-if-serial 語義是一回事,都是為了在不改變程序執(zhí)行結果的前提下,盡可能地提高程序執(zhí)行的并行度。只不過后者只能作用在單線程,而前者可以作用在正確同步的多線程環(huán)境下:

  • as-if-serial 語義保證單線程內程序的執(zhí)行結果不被改變,Happens-before 關系保證正確同步的多線程程序的執(zhí)行結果不被改變。
  • as-if-serial 語義給編寫單線程程序的程序員創(chuàng)造了一個幻境:單線程程序是按程序的順序來執(zhí)行的。Happens-before 關系給編寫正確同步的多線程程序的程序員創(chuàng)造了一個幻境:正確同步的多線程程序是按 Happens-before 指定的順序來執(zhí)行的。

以上就是JMM核心概念之Happens-before原則的詳細內容,更多關于JMM Happens-before的資料請關注腳本之家其它相關文章!

相關文章

  • Java數(shù)組中的元素刪除并實現(xiàn)向前移的代碼

    Java數(shù)組中的元素刪除并實現(xiàn)向前移的代碼

    這篇文章主要介紹了Java數(shù)組中的元素刪除并實現(xiàn)向前移的代碼的相關資料,需要的朋友可以參考下
    2016-05-05
  • 詳解Java對象結構與對象鎖的升級

    詳解Java對象結構與對象鎖的升級

    這篇文章主要為大家詳細介紹了Java對象結構與對象鎖的升級,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助
    2022-03-03
  • SpringBoot啟動報錯的11個高頻問題排查與解決終極指南

    SpringBoot啟動報錯的11個高頻問題排查與解決終極指南

    這篇文章主要為大家詳細介紹了SpringBoot啟動報錯的11個高頻問題的排查與解決,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下
    2025-03-03
  • Springboot調整接口響應返回時長詳解(解決響應超時問題)

    Springboot調整接口響應返回時長詳解(解決響應超時問題)

    當后端對于數(shù)據(jù)量較大的處理或是某些耗時的操作時,需要先對請求接口的請求進行響應,下面這篇文章主要給大家介紹了關于Springboot調整接口響應返回時長(解決響應超時問題)的相關資料,需要的朋友可以參考下
    2023-01-01
  • mybatis注解開發(fā)使用foreach方式

    mybatis注解開發(fā)使用foreach方式

    這篇文章主要介紹了mybatis注解開發(fā)使用foreach方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-08-08
  • Spring?Security配置保姆級教程

    Spring?Security配置保姆級教程

    Spring?Security是一個功能強大且可高度自定義的身份驗證和訪問控制框架。它是保護基于Spring的應用程序的事實上的標準。Spring?Security是一個專注于為Java應用程序提供身份驗證和授權的框架
    2023-02-02
  • Springboot+aop實現(xiàn)配置多數(shù)據(jù)源的示例代碼

    Springboot+aop實現(xiàn)配置多數(shù)據(jù)源的示例代碼

    本文介紹了如何使用SpringAOP和注解實現(xiàn)動態(tài)數(shù)據(jù)源切換,通過自定義注解和ThreadLocal存儲數(shù)據(jù)上下文信息,重寫AbstractRoutingDataSource類并使用自定義切面來實現(xiàn)動態(tài)數(shù)據(jù)源的切換,感興趣的可以了解一下
    2024-11-11
  • Spring MVC+FastJson+Swagger集成的完整實例教程

    Spring MVC+FastJson+Swagger集成的完整實例教程

    這篇文章主要給大家分享介紹了關于Spring MVC+FastJson+Swagger集成的完整實例教程,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。
    2018-04-04
  • Idea打不了斷點如何解決

    Idea打不了斷點如何解決

    這篇文章主要介紹了Idea打不了斷點如何解決的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-03-03
  • SpringCloudStream中的消息分區(qū)數(shù)詳解

    SpringCloudStream中的消息分區(qū)數(shù)詳解

    這篇文章主要介紹了SpringCloudStream中的消息分區(qū)數(shù),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-12-12

最新評論

呼和浩特市| 荃湾区| 兴文县| 会宁县| 庆城县| 噶尔县| 霞浦县| 卢龙县| 隆昌县| 连平县| 泉州市| 南充市| 岢岚县| 通山县| 辉南县| 榆中县| 西平县| 札达县| 怀安县| 白朗县| 漠河县| 天峨县| 武乡县| 壤塘县| 班玛县| 五河县| 昭觉县| 宾川县| 敖汉旗| 苍南县| 五大连池市| 宁国市| 商河县| 洪泽县| 兴城市| 安泽县| 南投市| 沭阳县| 阳高县| 保亭| 兴化市|