我真不想學(xué) Happens - Before 了!
這個(gè)我想是大家學(xué)習(xí) Java 并發(fā)編程中非常容易忽略的一個(gè)點(diǎn),為什么,因?yàn)樘橄罅恕?/p>
我剛開始學(xué)習(xí)的時(shí)候遇到 happens-before 的時(shí)候也是不明覺厲,"哪來(lái)的這么一個(gè)破玩意"!
happens - before 不像是什么 Java 并發(fā)工具類能夠淺顯易懂,容易上手。happens - before 重在理解。
happens - before 和 JMM 也就是 Java 內(nèi)存模型有關(guān),所以我們需要先從 JMM 入手,才能更好的理解 happens - before 原則。
JMM 的設(shè)計(jì)
JMM 是 JVM 的基礎(chǔ),因?yàn)?JVM 中的堆區(qū)、方法區(qū)、棧區(qū)都是建立在 JMM 基礎(chǔ)上的,你可能還是不理解這是怎么回事,沒(méi)關(guān)系,我們先來(lái)看一下 JMM 的模型。
JVM 的劃分想必大家應(yīng)該了然于胸,這里就不再贅述了,我們主要說(shuō)一下 JVM 各個(gè)區(qū)域在 JMM 中的分布。JVM 中的棧區(qū)包括局部變量和操作數(shù)棧,局部變量在各個(gè)線程之間都是獨(dú)立存在的,即各個(gè)線程之間不會(huì)互相干擾,變量的值只會(huì)受到當(dāng)前線程的影響,這在《Java 并發(fā)編程實(shí)戰(zhàn)》中被稱為線程封閉。
然而,線程之間的共享變量卻存儲(chǔ)在主內(nèi)存(Main Memory)中,共享變量是 JVM 堆區(qū)的重要組成部分。
那么,共享變量是如何被影響的呢?
這里其實(shí)有操作系統(tǒng)層面解決進(jìn)程通信的一種方式:共享內(nèi)存,主內(nèi)存其實(shí)就是共享內(nèi)存。
之所以說(shuō)共享變量能夠被影響,是由于每個(gè) Java 線程在執(zhí)行代碼的過(guò)程中,都會(huì)把主內(nèi)存中的共享變量 load 一份副本到工作內(nèi)存中。
當(dāng)每個(gè) Java 線程修改工作內(nèi)存中的共享變量副本后,會(huì)再把共享變量 store 到主存中,由于不同線程對(duì)共享變量的修改不一樣,而且每個(gè)線程對(duì)共享變量的修改彼此不可見,所以最后覆蓋內(nèi)存中共享變量的值的時(shí)候可能會(huì)出現(xiàn)重復(fù)覆蓋的現(xiàn)象,這也是共享變量不安全的因素。
由于 JMM 的這種設(shè)計(jì),導(dǎo)致出現(xiàn)了我們經(jīng)常說(shuō)的可見性和有序性問(wèn)題。
關(guān)于可見性和 Java 并發(fā)編程中如何解決可見性問(wèn)題,我們?cè)?volatile 這篇文章中已經(jīng)詳細(xì)介紹過(guò)了。實(shí)際上,在 volatile 解決可見性問(wèn)題的同時(shí),也是遵循了 happens - before 原則的。
happens - before 原則
JSR-133 使用 happens - before 原則來(lái)指定兩個(gè)操作之間的執(zhí)行順序。這兩個(gè)操作可以在同一個(gè)線程內(nèi),也可以在不同線程之間。同一個(gè)線程內(nèi)是可以使用 as-if-serial 語(yǔ)義來(lái)保證可見性的,所以 happens - before 原則更多的是用來(lái)解決不同線程之間的可見性。
JSR - 133 對(duì) happens - before 關(guān)系有下面這幾條定義,我們分別來(lái)解釋下。
程序順序規(guī)則
Each action in a thread happens-before every subsequent action in that thread.
每個(gè)線程在執(zhí)行指令的過(guò)程中都相當(dāng)于是一條順序執(zhí)行流程:取指令,執(zhí)行,指向下一條指令,取指令,執(zhí)行。
而程序順序規(guī)則說(shuō)的就是在同一個(gè)順序執(zhí)行流中,會(huì)按照程序代碼的編寫順序執(zhí)行代碼,編寫在前面的代碼操作要 happens - before 編寫在后面的代碼操作。
這里需要特別注意??的一點(diǎn)就是:這些操作的順序都是對(duì)于同一個(gè)線程來(lái)說(shuō)的。
monitor 規(guī)則
An unlock on a monitor happens-before every subsequent lock on that monitor.
這是一條對(duì) monitor 監(jiān)視器的規(guī)則,主要是面向 lock 和 unlock 也就是加鎖和解鎖來(lái)說(shuō)明的。這條規(guī)則是對(duì)于同一個(gè) monitor 來(lái)說(shuō),這個(gè) monitor 的解鎖(unlock)要 happens - before 后面對(duì)這個(gè)監(jiān)視器的加鎖(lock)。
比如下面這段代碼
- class monitorLock {
- private int value = 0;
- public synchronized int getValue() {
- return value;
- }
- public synchronized void setValue(int value) {
- this.value = value;
- }
- }
在這段代碼中,getValue 和 setValue 這兩個(gè)方法使用了同一個(gè) monitor 鎖,假設(shè) A 線程正在執(zhí)行 getValue 方法,B 線程正在執(zhí)行 setValue 方法。monitor 的原則會(huì)規(guī)定線程 B 對(duì) value 值的修改,能夠直接對(duì)線程 A 可見。如果 getValue 和 setValue 沒(méi)有 synchronized 關(guān)鍵字進(jìn)行修飾的話,則不能保證線程 B 對(duì) value 值的修改,能夠?qū)€程 A 可見。
monitor 的規(guī)則對(duì)于 synchronized 語(yǔ)義和 ReentrantLock 中的 lock 和 unlock 的語(yǔ)義是一樣的。
volatile 規(guī)則
A write to a volatile ?eld happens-before every subsequent read of that volatile.
這是一條對(duì) volatile 的規(guī)則,它說(shuō)的是對(duì)一個(gè) volatile 變量的寫操作 happens - before 后續(xù)任意對(duì)這個(gè)變量的讀操作。
嗯,這條規(guī)則其實(shí)就是在說(shuō) volatile 語(yǔ)義的規(guī)則,因?yàn)閷?duì) volatile 的寫和讀之間會(huì)增加 memory barrier ,也就是內(nèi)存屏障。
內(nèi)存屏障也叫做柵欄,它是一種底層原語(yǔ)。它使得 CPU 或編譯器在對(duì)內(nèi)存進(jìn)行操作的時(shí)候, 要嚴(yán)格按照一定的順序來(lái)執(zhí)行, 也就是說(shuō)在 memory barrier 之前的指令和 memory barrier 之后的指令不會(huì)由于系統(tǒng)優(yōu)化等原因而導(dǎo)致亂序。
線程 start 規(guī)則
A call to start() on a thread happens-before any actions in the started thread.
這條規(guī)則也是適用于同一個(gè)線程,對(duì)于相同線程來(lái)說(shuō),調(diào)用線程 start 方法之前的操作都 happens - before start 方法之后的任意操作。
這條原則也可以這樣去理解:調(diào)用 start 方法時(shí),會(huì)將 start 方法之前所有操作的結(jié)果同步到主內(nèi)存中,新線程創(chuàng)建好后,需要從主內(nèi)存獲取數(shù)據(jù)。這樣在 start 方法調(diào)用之前的所有操作結(jié)果對(duì)于新創(chuàng)建的線程都是可見的。
我來(lái)畫幅圖給你看。
可以看到,線程 A 在執(zhí)行 ThreadB.start 方法之前會(huì)對(duì)共享變量進(jìn)行修改,修改之后的共享變量會(huì)直接刷新到內(nèi)存中,然后線程 A 執(zhí)行 ThreadB.start 方法,緊接著線程 B 會(huì)從內(nèi)存中讀取共享變量。
線程 join 規(guī)則
All actions in a thread happen-before any other thread successfully returns from a join() on that thread.
這條規(guī)則是對(duì)多條線程來(lái)說(shuō)的:如果線程 A 執(zhí)行操作 ThreadB.join() 并成功返回,那么線程 B 中的任意操作都 happens - before 于線程 A 從 ThreadB.join 操作成功返回。
假設(shè)有兩個(gè)線程 s、t,在線程 s 中調(diào)用 t.join() 方法。則線程 s 會(huì)被掛起,等待 t 線程運(yùn)行結(jié)束才能恢復(fù)執(zhí)行。當(dāng)t.join() 成功返回時(shí),s 線程就知道 t 線程已經(jīng)結(jié)束了。所以根據(jù)本條原則,在 t 線程中對(duì)共享變量的修改,對(duì) s 線程都是可見的。類似的還有 Thread.isAlive 方法也可以檢測(cè)到一個(gè)線程是否結(jié)束。
線程傳遞規(guī)則
If an action a happens-before an action b, and b happens before an action c, then a happensbefore c.
這是 happens - before 的最后一個(gè)規(guī)則,它主要說(shuō)的是操作之間的傳遞性,也就是說(shuō),如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
線程傳遞規(guī)則不像上面其他規(guī)則有單獨(dú)的用法,它主要是和 volatile 規(guī)則、start 規(guī)則和 join 規(guī)則一起使用。
和 volatile 規(guī)則一起使用
比如現(xiàn)在有四個(gè)操作:普通寫、volatile 寫、volatile 讀、普通讀,線程 A 執(zhí)行普通寫和 volatile 寫,線程B 執(zhí)行volatile 讀和普通讀,根據(jù)程序的順序性可知,普通寫 happens - before volatile 寫,volatile 讀 happens - before 普通讀,根據(jù) volatile 規(guī)則可知,線程的 volatile 寫 happens - before volatile 讀和普通讀,然后根據(jù)線程傳遞規(guī)則可知,普通寫也 happens - before 普通讀。
和 start() 規(guī)則一起使用
和 start 規(guī)則一起使用,其實(shí)我們?cè)谏厦婷枋?start 規(guī)則的時(shí)候已經(jīng)描述了,只不過(guò)上面那幅圖少畫了一條線,也就是 ThreadB.start happens - before 線程 B 讀共享變量,由于 ThreadB.start 要 happens - before 線程 B 開始執(zhí)行,然而從程序定義的順序來(lái)說(shuō),線程 B 的執(zhí)行 happens - before 線程 B 讀共享變量,所以根據(jù)線程傳遞規(guī)則來(lái)說(shuō),線程 A 修改共享變量 happens - before 線程 B 讀共享變量,如下圖所示。
和 join() 規(guī)則一起使用
假設(shè)線程 A 在執(zhí)行的過(guò)程中,通過(guò)執(zhí)行 ThreadB.join 來(lái)等待線程 B 終止。同時(shí),假設(shè)線程 B 在終止之前修改了一些共享變量,線程 A 從 ThreadB.join 返回后會(huì)讀這些共享變量。
在上圖中,2 happens - before 4 由 join 規(guī)則來(lái)產(chǎn)生,4 happens - before 5 是程序順序規(guī)則,所以根據(jù)線程傳遞規(guī)則,將會(huì)有 2 happens - before 5,這也意味著,線程 A 執(zhí)行操作 ThreadB.join 并成功返回后,線程 B 中的任意操作將對(duì)線程 A 可見。