自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

JMM 最最最核心的概念:Happens-before 原則

開發(fā) 后端
Happens-before 是 JMM 最核心的概念。對(duì)應(yīng) Java 程序員來說,理解 Happens-before 是理解 JMM 的關(guān)鍵。

[[398234]]

本文轉(zhuǎn)載自微信公眾號(hào)「飛天小牛肉」,作者飛天小牛肉。轉(zhuǎn)載本文請(qǐng)聯(lián)系飛天小牛肉公眾號(hào)。

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

Happens-before 是 JMM 最核心的概念。對(duì)應(yīng) Java 程序員來說,理解 Happens-before 是理解 JMM 的關(guān)鍵。

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

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

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

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

生來如此。

JMM 設(shè)計(jì)者的難題與完美的解決方案

上篇文章「跬步千里」詳解 Java 內(nèi)存模型與原子性、可見性、有序性 我們學(xué)習(xí)了 JMM 及其三大性質(zhì),事實(shí)上,從 JMM 設(shè)計(jì)者的角度來看,可見性和有序性其實(shí)是互相矛盾的兩點(diǎn):

一方面,對(duì)于程序員來說,我們希望內(nèi)存模型易于理解、易于編程,為此 JMM 的設(shè)計(jì)者要為程序員提供足夠強(qiáng)的內(nèi)存可見性保證,專業(yè)術(shù)語(yǔ)稱之為 “強(qiáng)內(nèi)存模型”。

而另一方面,編譯器和處理器則希望內(nèi)存模型對(duì)它們的束縛越少越好,這樣它們就可以做盡可能多的優(yōu)化(比如重排序)來提高性能,因此 JMM 的設(shè)計(jì)者對(duì)編譯器和處理器的限制要盡可能地放松,專業(yè)術(shù)語(yǔ)稱之為 “弱內(nèi)存模型”。

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

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

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

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

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

針對(duì)這個(gè)第 1 條定義,我來舉個(gè)例子:

  1. // 以下操作在線程 A 中執(zhí)行 
  2. i = 1; // a 
  3.  
  4. // 以下操作在線程 B 中執(zhí)行 
  5. j = i; // b 
  6.  
  7. // 以下操作在線程 C 中執(zhí)行 
  8. i = 2; // c 

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

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

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

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

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

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

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

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

1)A Happens-before B

2)B Happens-before C

3)A Happens-before C

可以看出來,在 3 個(gè) Happens-before 關(guān)系中,第 2 個(gè)和第 3 個(gè)是必需的,但第 1 個(gè)是不必要的。

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

看下面這張 JMM 的設(shè)計(jì)圖更直觀:

圖片來源《Java 并發(fā)編程的藝術(shù)》

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

圖片來源《Java 并發(fā)編程的藝術(shù)》

8 條 Happens-before 規(guī)則

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

1)程序次序規(guī)則(Program Order Rule):在一個(gè)線程內(nèi),按照控制流順序,書寫在前面的操作先行發(fā)生(Happens-before)于書寫在后面的操作。注意,這里說的是控制流順序而不是程序代碼順序,因?yàn)橐紤]分支、循環(huán)等結(jié)構(gòu)。

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

  1. synchronized (this) { // 此處自動(dòng)加鎖 
  2.  if (x < 1) { 
  3.         x = 1; 
  4.     }       
  5. } // 此處自動(dòng)解鎖 

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

A Happens-before B

B Happens-before C

A Happens-before C

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

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

舉個(gè)例子:

  1. synchronized (this) { // 此處自動(dòng)加鎖 
  2.  if (x < 1) { 
  3.         x = 1; 
  4.     }       
  5. } // 此處自動(dòng)解鎖 

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

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

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

舉個(gè)例子:

假設(shè)線程 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 設(shè)置的“a=42”對(duì)線程 B 是可見的。

看下圖:

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

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

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

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

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

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

“時(shí)間上的先發(fā)生” 與 “先行發(fā)生”

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

一個(gè)操作 “時(shí)間上的先發(fā)生” 是否就代表這個(gè)操作會(huì)是“先行發(fā)生” 呢?一個(gè)操作 “先行發(fā)生” 是否就能推導(dǎo)出這個(gè)操作必定是“時(shí)間上的先發(fā)生”呢?

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

舉兩個(gè)例子論證一下:

  1. private int value = 0; 
  2.  
  3. // 線程 A 調(diào)用 
  4. pubilc void setValue(int value){     
  5.     this.value = value; 
  6.  
  7. // 線程 B 調(diào)用 
  8. public int getValue(){ 
  9.     return value; 

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

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

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

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

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

因?yàn)闆]有一個(gè)適用的 Happens-before 規(guī)則,所以第 8 條規(guī)則傳遞性也無從談起。

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

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

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

再來看一個(gè)例子:

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

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

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

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

這樣,綜上兩例,我們可以得出這樣一個(gè)結(jié)論:Happens-before 原則與時(shí)間先后順序之間基本沒有因果關(guān)系,所以我們?cè)诤饬坎l(fā)安全問題的時(shí)候,盡量不要受時(shí)間順序的干擾,一切必須以 Happens-before 原則為準(zhǔn)。

Happens-before 與 as-if-serial

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

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

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

as-if-serial 語(yǔ)義保證單線程內(nèi)程序的執(zhí)行結(jié)果不被改變,Happens-before 關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變。

as-if-serial 語(yǔ)義給編寫單線程程序的程序員創(chuàng)造了一個(gè)幻境:?jiǎn)尉€程程序是按程序的順序來執(zhí)行的。Happens-before 關(guān)系給編寫正確同步的多線程程序的程序員創(chuàng)造了一個(gè)幻境:正確同步的多線程程序是按 Happens-before 指定的順序來執(zhí)行的。

References

《Java 并發(fā)編程的藝術(shù)》

《深入理解 Java 虛擬機(jī) - 第 3 版》

責(zé)任編輯:武曉燕 來源: 飛天小牛肉
相關(guān)推薦

2022-06-27 08:01:45

Java內(nèi)存模型

2020-05-28 07:50:18

重排序happens-befCPU

2024-04-23 00:00:00

SpringBoot監(jiān)聽器

2022-06-08 13:54:23

指令重排Java

2021-07-29 07:51:43

工具 HappensBefore

2013-03-01 09:53:40

軟件開發(fā)

2021-08-11 11:25:22

happens - bJava代碼

2024-01-09 08:24:47

JMM核心線程

2015-08-18 08:55:03

redux核心

2021-01-27 08:37:22

IDEAProjectIntelliJ ID

2021-08-31 07:02:34

數(shù)據(jù)響應(yīng)Vue偵測(cè)數(shù)據(jù)變化

2020-10-13 21:25:15

DevOps核心

2019-06-03 10:53:49

MySQL硬盤數(shù)據(jù)庫(kù)

2020-10-12 08:09:39

JMM理解

2021-02-19 08:38:36

Kubernetes容器化分布式

2015-08-20 09:45:56

可視化

2019-06-05 13:00:36

2021-11-14 23:06:49

Python代碼開發(fā)

2023-10-22 23:28:34

2019-05-28 12:03:59

vuejavascript前端
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)