這篇文章主要介紹“什么是Happens-before原則”,在日常操作中,相信很多人在什么是Happens-before原則問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對(duì)大家解答”什么是Happens-before原則”的疑惑有所幫助!接下來,請(qǐng)跟著小編一起來學(xué)習(xí)吧!
成都創(chuàng)新互聯(lián)公司自2013年創(chuàng)立以來,是專業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項(xiàng)目成都網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、外貿(mào)網(wǎng)站建設(shè)網(wǎng)站策劃,項(xiàng)目實(shí)施與項(xiàng)目整合能力。我們以讓每一個(gè)夢(mèng)想脫穎而出為使命,1280元古城做網(wǎng)站,已為上家服務(wù),為古城各地企業(yè)和個(gè)人服務(wù),聯(lián)系電話:028-86922220
上篇文章「跬步千里」詳解 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ù)語稱之為 “強(qiáng)內(nèi)存模型”。
而另一方面,編譯器和處理器則希望內(nèi)存模型對(duì)它們的束縛越少越好,這樣它們就可以做盡可能多的優(yōu)化(比如重排序)來提高性能,因此 JMM 的設(shè)計(jì)者對(duì)編譯器和處理器的限制要盡可能地放松,專業(yè)術(shù)語稱之為 “弱內(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 語義只能作用在單線程,這里提到的兩個(gè)操作 A 和 B 既可以是在一個(gè)線程之內(nèi),也可以是在不同線程之間。也就是說,Happens-before 提供跨線程的內(nèi)存可見性保證。
針對(duì)這個(gè)第 1 條定義,我來舉個(gè)例子:
// 以下操作在線程 A 中執(zhí)行 i = 1; // a // 以下操作在線程 B 中執(zhí)行 j = i; // b // 以下操作在線程 C 中執(zhí)行 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í)行。
int a = 1; // A int b = 2; // B 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í),可以這么簡單的理解,為了避免 Java 程序員為了理解 JMM 提供的內(nèi)存可見性保證而去學(xué)習(xí)復(fù)雜的重排序規(guī)則以及這些規(guī)則的具體實(shí)現(xiàn)方法,JMM 就出了這么一個(gè)簡單易懂的 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è)很好理解,符合我們的邏輯思維。比如我們上面舉的例子:
synchronized (this) { // 此處自動(dòng)加鎖 if (x < 1) { x = 1; } } // 此處自動(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è)例子:
synchronized (this) { // 此處自動(dòng)加鎖 if (x < 1) { x = 1; } } // 此處自動(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 語義的增強(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ì)此線程的終止檢測,我們可以通過 Thread 對(duì)象的 join() 方法是否結(jié)束、Thread 對(duì)象的 isAlive() 的返回值等手段檢測線程是否已經(jīng)終止執(zhí)行。
6)線程中斷規(guī)則(Thread Interruption Rule):對(duì)線程 interrupt() 方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過 Thread 對(duì)象的 interrupted() 方法檢測到是否有中斷發(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é)論。
上述 8 種規(guī)則中,還不斷提到了時(shí)間上的先后,那么,“時(shí)間上的先發(fā)生” 與 “先行發(fā)生(Happens-before)” 到底有啥區(qū)別?
一個(gè)操作 “時(shí)間上的先發(fā)生” 是否就代表這個(gè)操作會(huì)是“先行發(fā)生” 呢?一個(gè)操作 “先行發(fā)生” 是否就能推導(dǎo)出這個(gè)操作必定是“時(shí)間上的先發(fā)生”呢?
很遺憾,這兩個(gè)推論都是不成立的。
舉兩個(gè)例子論證一下:
private int value = 0; // 線程 A 調(diào)用 pubilc void setValue(int value){ this.value = value; } // 線程 B 調(diào)用 public int getValue(){ 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è)問題也很簡單?既然不滿足 Happens-before 原則,那我修改下讓它滿足不就行了。比如說把 Getter/Setter 方法都用 synchronized 修飾,這樣就可以套用管程鎖定規(guī)則;再比如把 value 定義為 volatile 變量,這樣就可以套用 volatile 變量規(guī)則等。
這個(gè)例子,就論證了一個(gè)操作 “時(shí)間上的先發(fā)生” 不代表這個(gè)操作會(huì)是 “先行發(fā)生(Happens-before)”。
再來看一個(gè)例子:
// 以下操作在同一個(gè)線程中執(zhí)行 int i = 1; int j = 2;
假設(shè)這段代碼中的兩條賦值語句在同一個(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)。
綜上,我覺得其實(shí)讀懂了下面這句話也就讀懂了 Happens-before 了,這句話上文也出現(xiàn)過幾次:JMM 其實(shí)是在遵循一個(gè)基本原則,即只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。
再回顧下 as-if-serial 語義:不管怎么重排序,單線程環(huán)境下程序的執(zhí)行結(jié)果不能被改變。
各位發(fā)現(xiàn)沒有?本質(zhì)上來說 Happens-before 關(guān)系和 as-if-serial 語義是一回事,都是為了在不改變程序執(zhí)行結(jié)果的前提下,盡可能地提高程序執(zhí)行的并行度。只不過后者只能作用在單線程,而前者可以作用在正確同步的多線程環(huán)境下:
as-if-serial 語義保證單線程內(nèi)程序的執(zhí)行結(jié)果不被改變,Happens-before 關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變。
as-if-serial 語義給編寫單線程程序的程序員創(chuàng)造了一個(gè)幻境:單線程程序是按程序的順序來執(zhí)行的。Happens-before 關(guān)系給編寫正確同步的多線程程序的程序員創(chuàng)造了一個(gè)幻境:正確同步的多線程程序是按 Happens-before 指定的順序來執(zhí)行的。
到此,關(guān)于“什么是Happens-before原則”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!