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