Java記憶體模型與執行緒 深入理解Java虛擬機器總結
在許多情況下,讓計算機同時去做幾件事情,不僅是因為計算機的運算能力強大了,還有一個很重要的原因是計算機的運算速度與它的儲存和通訊子系統速度的差距太大, 大量的時間都花費在磁碟I/O、網路通訊或者資料庫訪問上。 如果不希望處理器在大部分時間裡都處於等待其他資源的狀態,就必須使用一些手段去把處理器的運算能力 ” 壓榨 ” 出來, 否則就會造成很大的浪費,而計算機同時處理幾項任務則是最容易想到、也被證明是非常有效的 “ 壓榨 ” 手段。 除了充分利用計算機處理器的能力外,一個服務端同時對多個客戶端提供服務則是另一個更具體的併發應用場景。衡量一個服務效能的高低好壞,每秒事務處理數(Transactions Per Second,TPS)是最重要的指標之一,它代表著一秒內服務端平均能響應的請求總數,而 TPS 值與程式的併發能力又有非常密切的關係。對於計算量相同的任務,程式執行緒併發協調得越有條不紊,效率自然就會越高;反之,執行緒之間頻繁阻塞甚至死鎖,將會大大降低程式的併發能力。 服務端是 Java 語言最擅長的領域之一,這個領域的應用佔了 Java 應用中最大的一塊份額,不過如何寫好併發應用程式卻又是服務端程式開發的難點之一,處理好併發方面的問題通常需要更多的編碼經驗來支援。幸好 Java 語言和虛擬機器提供了許多工具,把併發程式設計的門檻降低了不少。並且各種中介軟體伺服器、各類框架都努力地替程式設計師處理儘可能多的執行緒併發細節,使得程式設計師在編碼時能更關注業務邏輯,而不是花費大部分時間去關注此服務會同時被多少人呼叫、如何協調硬體資源。無論語言、中介軟體和框架如何先進,開發人員都不能期望它們能獨立完成所有併發處理的事情,瞭解併發的內幕也是成為一個高階程式設計師不可缺少的課程。
硬體的效率與一致性
在正式講解 Java 虛擬機器併發相關的知識之前,我們先花費一點時間去了解一下物理計算機中的併發問題,物理機遇到的併發問題與虛擬機器中的情況有不少相似之處,物理機對併發的處理方案對於虛擬機器的實現也有相當大的參考意義。
“讓計算機併發執行若干個運算任務” 與 “更充分地利用計算機處理器的效能” 之間的因果關係,看起來順理成章,實際上它們之間的關係並沒有想象中的那麼簡單,其中一個重要的複雜性來源是絕大多數的運算任務都不可能只靠處理器 “計算” 就能完成,處理器至少要與記憶體互動,如讀取運算資料、儲存運算結果等,這個 I/O 操作是很難消除的(無法僅靠暫存器來完成所有運算任務)。由於計算機的儲存裝置與處理器的運算速度有幾個數量級的差距,所以現代計算機系統都不得不加入一層讀寫速度儘可能接近處理器運算速度的快取記憶體(Cache)來作為記憶體與處理器之間的緩衝:將運算需要使用到的資料複製到快取中,讓運算能快速進行,當運算結束後再從快取同步回記憶體之中,這樣處理器就無須等待緩慢的記憶體讀寫了。
基於快取記憶體的儲存互動很好地理解了處理器與記憶體的速度矛盾,但是也為計算機系統帶來了更高的複雜度,因為它引入了一個新的問題:快取一致性(Cache Coherence)。在多處理器系統中,每個處理器都有自己的快取記憶體,而它們又共享同一主記憶體(Main Memory),如圖 12-1 所示。當多個處理器的運算任務都涉及同一塊主記憶體區域時,將可能導致各自的快取資料不一致,如果真的發生這種情況,那同步回到主記憶體時以誰的快取資料為準呢?為了解決一致性的問題,需要各個處理器訪問快取時都遵循一些協議,在讀寫時要根據協議來進行操作,這類協議有 MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly 及 Dragon Protocol 等。在本章中將會多次提到的 “記憶體模型” 一詞,可以理解為在特定的操作協議下,對特定的記憶體或快取記憶體進行讀寫訪問的過程抽象。不同架構的物理機器可以擁有不一樣的記憶體模型,而 Java 虛擬機器也有自己的記憶體模型,並且這裡介紹的記憶體訪問操作與硬體的快取訪問操作具有很高的可比性。
除了增加快取記憶體之外,為了使得處理器內部的運算單元能儘量被充分利用,處理器可能會對輸入程式碼進行亂序執行(Out-Of-Order Execution)優化,處理器會在計算之後將亂序執行的結果充足,保證該結果與順序執行的結果是一致的,但並不保證程式中各個語句計算的先後順序與輸入程式碼中的順序一致,因此,如果存在一個計算任務依賴另外一個計算任務的中間結果,那麼其順序並不能靠程式碼的先後順序來保證。與處理器的亂序執行優化型別,Java 虛擬機器的即時編譯器中有有類似的指令重排序(Instruction Reorder)優化。
Java 記憶體模型
Java 虛擬機器規範中試圖定義一種 Java 記憶體模型(Java Memory Model,JMM)來遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓 Java 程式在各種平臺下都能達到一致的記憶體訪問效果。在此之前,主流程式語言(如 C/C++ 等)直接使用物理硬體和作業系統的記憶體模型,因此,會由於不同平臺上記憶體模型的差異,有可能導致程式在一套平臺上併發完全正常,而在另外一套平臺上併發訪問卻經常出錯,因此在某些場景就必須針對不同的平臺來編寫程式。
定義 Java 記憶體模型並非一件容易的事情,這個模型必須定義得足夠嚴謹,才能讓 Java 的併發記憶體訪問操作不會產生歧義;但是,也必須定義得足夠寬鬆,使得虛擬機器的實現有足夠的自由空間去利用硬體的各種特性(暫存器、快取記憶體和指令集中某些特有的指令)來獲取更好的執行速度。經過長時間的驗證和修補,在 JDK 1.5(實現了 JSR-133)釋出後,Java 記憶體模型已經成熟和完善起來了。
主記憶體與工作記憶體
Java 記憶體模型的主要目標是定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節。此處的變數(Variables)與 Java 程式設計中所說的變數有所區別,它包括了例項欄位、靜態欄位和構成陣列物件的元素,但不包括區域性變數與方法引數,因為後者是執行緒私有的,不會被共享,自然就不會存在競爭問題。為了獲得較好的執行效能,Java 記憶體模型並沒有限制執行引擎使用處理器的特定暫存器或快取和主記憶體進行互動,也沒有限制即時編譯器進行調整程式碼執行順序這類優化措施。
Java 記憶體模型規定了所有的變數都儲存在主記憶體(Main Memory)中(此處的主記憶體與介紹物理硬體時的主記憶體名字一樣,兩者也可以互相類比,但此處僅是虛擬機器記憶體的一部分)。每條執行緒還有自己的工作記憶體(Working Memory,可與前面講的處理器快取記憶體類比),執行緒的工作記憶體中儲存了被該執行緒使用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作(讀取、賦值等)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞均需要通過主記憶體來完成,執行緒、主記憶體、工作記憶體三者的互動關係如圖 12-2 所示。
這裡所講的主記憶體、工作記憶體與前面所講的 Java 記憶體區域的 Java 堆、棧、方法區等並不是同一個層次的記憶體劃分,這兩者基本上是沒有關係的,如果兩者一定要勉強對應起來,那從變數、主記憶體、工作記憶體的定義來看,主記憶體主要對應於 Java 堆中的物件例項資料部分,而工作記憶體則對應於虛擬機器棧中的部分割槽域。從更低層次上說,主記憶體就直接對應於物理硬體的記憶體,而為了獲取更高的執行速度,虛擬機器(甚至是硬體系統本身的優化措施)可能會讓工作記憶體優先儲存於暫存器和快取記憶體中,因為程式執行時主要訪問讀寫的是工作記憶體。
記憶體間互動操作
關於主記憶體與工作記憶體之間具體的互動協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步會主記憶體之類的實現細節,Java 記憶體模型中定義了以下 8 種操作來完成,虛擬機器實現時必須保證下面提及的每一種操作都是原子的、不可再分的(對於 double 和 long 型別的變數來說,load、store、read 和 write 操作在某些平臺上允許有例外)。
lock(鎖定):作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態。
unlock(解鎖):作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
read(讀取):作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的 load 動作使用。
load(載入):作用於工作記憶體的變數,它把 read 操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
use(使用):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到變數的值的位元組碼指令時將會執行這個操作。
assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
store(儲存):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的 write 操作使用。
write(寫入):作用於主記憶體的變數,它把 store 操作從工作記憶體中得到的變數的值放入主記憶體的變數中。
如果要把一個變數從主記憶體複製到工作記憶體,那就要順序地執行 read 和 load 操作,如果要把變數從工作記憶體同步回主記憶體,就要順序地執行 store 和 write 操作。注意,Java 記憶體模型只要求上述兩個操作必須按順序執行,而沒有保證是連續執行。也就是說,read 與 load 之間、store 與 write 之間是可插入其他指令的,如對主記憶體中的變數 a、b 進行訪問時,一種可能出現順序是 read a、read b、load b、load a。除此之外,Java 記憶體模型還規定了在執行上述 8 種基本操作時必須滿足如下規則:
a.不允許 read 和 load、store 和 write 操作之一單獨出現,即不允許一個變數從主記憶體讀取了但工作記憶體不接受,或者從工作記憶體發起回寫了但主記憶體不接受的情況出現。
b.不允許一個執行緒丟棄它的最近的 assign 操作,即變數在工作記憶體中改變了之後必須把該變化同步會主記憶體。
c.不允許一個執行緒無原因地(沒有發生過任何 assign 操作)把資料從執行緒的工作記憶體同步回主記憶體中。
d.一個新的變數只能在主記憶體中 “誕生”,不允許在工作記憶體中直接使用一個未被初始化(load 或 assign)的變數,換句話說,就是對一個變數實施 use、store 操作之前,必須先執行過了 assign 和 load 操作。
e.一個變數在同一個時刻只允許一條執行緒對其進行 lock 操作,但 lock 操作可以被同一條執行緒重複執行多次,多次執行 lock 後,只有執行相同次數的 unlock 操作,變數才會被解鎖。
f.如果對一個變數執行 lock 操作,那將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行 load 或 assign 操縱初始化變數的值。
g.如果一個變數事先沒有被 lock 操作鎖定,那就不允許對它執行 unlock 操作,也不允許去 unlock 一個被其他執行緒鎖定住的變數。
h.對一個變數執行 unlock 操作之前,必須先把此變數同步回主記憶體中(執行 store、write 操作)。
這 8 種記憶體訪問操作以及上述規則限定,再加上稍後介紹的對 volatile 的一些特殊規定,就已經完全確定了 Java 程式中哪些記憶體訪問操作在併發下是安全的。由於這種定義相當嚴謹但又十分煩瑣,實踐起來很麻煩,所以在後面筆者將介紹這種定義的一個等效判斷原則——先行發生原則,用來確定一個訪問在併發環境下是否安全。
對於volatile 型變數的特殊規則
關鍵字 volatile 可以說是 Java 虛擬機器提供的最輕量級的同步機制。
當一個變數定義為 volatile 之後,它將具備兩種特性,第一是保證此變數對所有執行緒的可見性,這裡的 “可見性” 是指當一條執行緒修改了這個變數的值,新值對於其他執行緒來說是可以立即得知的。而普通變數不能做到這一點,普通變數的值線上程間傳遞均需要通過主記憶體來完成,例如,執行緒 A 修改一個普通變數的值,然後向主記憶體進行回寫,另外一條執行緒 B 線上程 A 回寫完成了之後再從主記憶體進行讀取操作,新變數值才會對執行緒 B 可見。
關於 volatile 變數的可見性,經常會被開發人員誤解,認為以下描述成立:“volatile 變數對所有執行緒是立即可見的,對 volatile 變數所有的寫操作都能立刻反應到其他執行緒之中,換句話說,volatile 變數在各個執行緒中是一致的,所以基於 volatile 變數的運算在併發下是安全的”。這句話的論據部分並沒有錯,但是其論據並不能得出 “基於 volatile 變數的運算在併發下是安全的” 這個結論。volatile 變數在各個執行緒的工作記憶體中不存在一致性問題(在各個執行緒的工作記憶體中,volatile 變數也可以存在不一致的情況,但由於每次使用之前都要先重新整理,執行引擎看不到不一致的情況,因此可以認為不存在不一致性問題),但是 Java 裡面的運算並非原子操作,導致 volatile 變數的運算在併發下一樣是不安全的。
由於 volatile 變數只能保證可見性,在不符合以下兩條規則的運算場景中,我們仍然需要通過加鎖(使用 synchronized 或 java.util.concurrent 中的原子類)來保證原子性。
a.運算結果並不依賴變數的當前值,或者能夠確保只有單一的執行緒修改變數的值。
b.變數不需要與其他狀態變數共同參與不變約束。
使用 volatile 變數的第二個語義是禁止指令重排序優化,普通的變數僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變數賦值操作的順序與程式程式碼中的執行順序一致。因為在一個執行緒的方法執行過程中無法感知到這點,這也就是 Java 記憶體模型中描述的所謂的 “執行緒內表現為序列的語義”(Within-Thread As-If-Serial Semantics)。
volatile的記憶體屏障策略非常嚴格保守,非常悲觀且毫無安全感的心態:在每個volatile寫操作前插入StoreStore屏障,在寫操作後插入StoreLoad屏障;在每個volatile讀操作前插入LoadLoad屏障,在讀操作後插入LoadStore屏障;由於記憶體屏障的作用,避免了volatile變數和其它指令重排序、執行緒之間實現了通訊,使得volatile表現出了鎖的特性。
那為何說它禁止指令重排序呢?從硬體架構上講,指令重排序是指 CPU 採用了允許將多條指令不按程式規定的順序分開發送給各相應電路單元處理。但並不是說指令任意重排,CPU 需要能正確處理指令依賴情況以保障程式能得出正確的執行結果。譬如指令 1 把地址 A 中的值加 10,指令 2 把地址 A 中的值乘以 2,指令 3 把地址 B 中的值減去 3,這時指令 1 和 指令 2 是有依賴的,它們之間的順序不能重排——(A + 10) 2 與 A 2 + 10 顯然不相等,但指令 3 可以重排到指令 1、2 之前或者中間,只要保證 CPU 執行後面依賴到 A、B 值的操作是能獲取到正確的 A 和 B 值即可。所以在本內 CPU 中,重排序看起來依然是有序的。因此 lock addl $0x0, (%esp) 指令把修改同步到記憶體時,意味著所有之前的操作都已經執行完成,這樣便形成了“指令重排序無法越過記憶體屏障” 的效果。
在本節的最後,我們回頭看一下 Java 記憶體模型中對 volatile 變數定義的特殊規則。假定 T 表示一個執行緒,V 和 W 分別表示兩個 volatile 型變數,那麼在進行 read、load、use、assign、store 和 write 操作時需要滿足如下規則:
a.只有當執行緒 T 對變數 V 執行的前一個動作是 load 的時候,執行緒 T 才能對變數 V 執行 use 動作;並且,只有當執行緒 T 對變數 V 執行的後一個動作是 use 的時候,執行緒 T 才能對變數 V 執行 load 動作。執行緒 T 對變數 V 的 use 動作可以認為是和執行緒 T 對變數 V 的 load、read 動作相關聯,必須連續一起出現(這套規則要求在工作記憶體中,每次使用 V 前都必須先從主記憶體重新整理最新的值,用於保證能看見其他執行緒對變數 V 所做的修改後的值)。
b只有當執行緒 T 對變數的前一個動作是 assign 的時候,執行緒 T 才能對變數 V 執行 store 動作;並且,只有當執行緒 T 對變數 V 執行的後一個動作是 store 的時候,執行緒 T 才能對變數 V 執行 assign 動作。執行緒 T 對變數 V 的 assign 動作可以認為是和執行緒 T 對變數 V 的 store、write 動作相關聯,必須連續一起出現(這條規則要求在工作記憶體中,每次修改 V 後都必須立刻同步回主記憶體中,用於保證其他執行緒可以看到自己對變數 V 所做的修改)。
c.假定動作 A 是執行緒 T 對變數 V 實施的 use 或 assign 動作,假定動作 F 是和動作 A 相關聯的 load 或 store 動作,假定動作 P 是和動作 F 相應的對變數 V 的 read 或 write 動作;類似的,假定動作 B 是執行緒 T 對變數 W 實施的 use 或 assign 動作,假定動作 G 是和動作 B 相關聯的 load 或 store 動作,假定動作 Q 是和動作 G 相應的對變數 W 的 read 或 write 動作。如果 A 先於 B,那麼 P 先於 Q(這條規則要求 volatile 修飾的變數不會被指令重排序優化,保證程式碼的執行順序與程式的順序相同)。
對於 long 和 double 型變數的特殊規則
Java 記憶體模型要求 lock、unlock、read、assign、use、store、write 這 8 個操作都具有原子性,但是對於 64 位的資料型別(long 和 double),在模型中特別定義了一條相對寬鬆的規定:允許虛擬機器將沒有被 volatile 修飾的 64 位資料的讀寫操作劃分為兩次 32 位的操作來進行,即允許虛擬機器實現選擇可以不保證 64 位資料型別的 load、store、read 和 write 這 4 個操作的原子性,這點就是所謂的 long 和 double 的非原子性協定(Nonatomic Treatment of double and long Variables)。
如果有多個執行緒共享一個並未宣告為 volatile 的 long 或 double 型別的變數,並且同時對它們進行讀取和修改操作,那麼某些執行緒可能會讀取到一個既非原值,也不是其他執行緒修改的值的代表了 “半個變數” 的數值。
不過這種讀取到 “半個變數” 的情況非常罕見(在目前商用 Java 虛擬機器中不會出現),因為 Java 記憶體模型雖然允許虛擬機器不把 long 和 double 變數的讀寫實現成原子操作,但允許虛擬機器選擇把這些操作實現為具有原子性的操作,而且還 “強烈建議” 虛擬機器這樣實現。在實際開發中,目前各種平臺下的商用虛擬機器幾乎都選擇把 64 位的資料的讀寫操作作為原子操作來對待,因此我們在編寫程式碼時一般不需要把用到的 long 和 double 變數專門宣告為 volatile。
原子性、可見性與有序性
介紹完 Java 記憶體模型的相關操作和規則,我們再整體回顧一下這個模型的特徵。Java 記憶體模型是圍繞著在併發過程中如何處理原子性、可見性和有序性這 3 個特徵來建立的,我們逐個來看一下哪些操作實現了這 3 個特性。
原子性(Atomicity):由 Java 記憶體模型來直接保證的原子性變數操作包括 read、load、assign、use、store 和 write,我們大致可以認為基本資料型別的訪問讀寫是具備原子性的(例外就是 long 和 double 的非原子性協定,讀者只要知道這件事就可以了,無須太過在意這些幾乎不會發生的例外情況)。
如果應用場景需要一個更大範圍的原子性保證(經常會遇到),Java 記憶體模型還提供了 lock 和 unlock 操作來滿足這種需求,儘管虛擬機器未把 lock 和 unlock 操作直接開放給使用者使用,但是卻提供了更高層次的位元組碼指令 monitorenter 和 monitorexit 來隱式地使用這兩個操作,這兩個位元組碼指令反映到 Java 程式碼中就是同步塊——synchronized 關鍵字,因此在 synchronized 塊之間的操作也具備原子性。
可見性(Visibility):可見性是指當一個執行緒修改了共享變數的值,其他執行緒能夠立即得知這個修改。上文在講解 volatile 變數的時候我們已詳細討論過這一點。Java 記憶體模型是通過在變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值這種依賴主記憶體作為傳遞媒介的方式來實現可見性的,無論是普通變數還是 volatile 變數都是如此,普通變數與 volatile 變數的區別是,volatile 的特殊規則保證了新值能立即同步到主記憶體,以及每次使用前立即從主記憶體重新整理。因此,可以說 volatile 保證了多執行緒操作時變數的可見性,而普通變數則不能保證這一點。
除了 volatile 之外,Java 還有兩個關鍵字能實現可見性,即 synchronized 和 final。同步塊的可見性是由 “對一個變數執行 unlock 操作之前,必須先把此變數同步會主記憶體中(執行 store、write 操作)” 這條規則獲得的,而 final 關鍵字的可見性是指:被 final 修飾的欄位在構造器中一旦初始化完成,並且構造器沒有把 “this” 的引用傳遞出去(this 引用逃逸是一件很危險的事情,其他執行緒有可能通過這個引用訪問到 “初始化了一半” 的物件),那在其他執行緒中就能看見 final 欄位的值。
有序性(Ordering):Java 記憶體模型的有序性在前面講解 volatile 時也詳細地討論過了,Java 程式中天然的有序性可以總結為一句話:如果在本執行緒內觀察,所有的操作都是有序的;如果在一個執行緒中觀察另一個執行緒,所有的操作都是無序的。前半句是指 “執行緒內表現為序列的語義” (Within-Thread As-If-Serial Semantics),後半句是指 “指令重排序” 現象和 “工作記憶體與主記憶體同步延遲” 現象。
Java 語言提供了 volatile 和 synchronized 兩個關鍵字來保證執行緒之間操作的有序性,volatile 關鍵字本身就包含了禁止指令重排序的語義,而 synchronized 則是由 “一個變數在同一個時刻只允許一條執行緒對其進行 lock 操作” 這條規則獲得的,這條規則決定了持有同一個鎖的兩個同步塊只能序列地進入。
先行發生原則
如果 Java 記憶體模型中所有的有序性都僅僅靠 volatile 和 synchronized 來完成,那麼有一些操作將會變得很煩瑣,但是我們在編寫 Java 併發程式碼的時候並沒有感覺到這一點,這是因為 Java 語言中有一個 “先行發生”(happens-before)的原則。這個原則非常重要,它是判斷資料是否存在競爭、執行緒是否安全的主要依據,依靠這個原則,我們可以通過幾條規則一攬子地解決併發環境下兩個操作之間是否可能存在衝突的所有問題。
現在就來看看 “先行發生” 原則指的是什麼。先行發生是 Java 記憶體模型中定義的兩項操作之間的偏序關係,如果說操作 A 先行發生與操作 B,其實就是說在發生操作 B 之前,操作 A 產生的影響能被操作 B 觀察到,“影響” 包括了修改了記憶體中共享變數的值、傳送了訊息、呼叫了方法等。
下面是 Java 記憶體模型下一些 “天然的” 先行發生關係,這些先行發生關係無須任何同步器協助就已經存在,可以在編碼中直接使用。如果兩個操作之間的關係不在此列,並且無法從下列規則推匯出來的話,它們就沒有順序性保障,虛擬機器可以對它們隨意地進行重排序。
程式次序規則(Program Order Rule):在一個執行緒內,按照程式程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作。準確地說,應該是控制流順序而不是程式程式碼順序,因為要考慮分支、迴圈等結果。
管程鎖定規則(Monitor Lock Rule):一個 unlock 操作先行發生於後面對同一個鎖的 lock 操作。這裡必須強調的是同一個鎖,而 “後面” 是指時間上的先後順序。
volatile 變數規則(Volatile Variable Rule):對一個 volatile 變數的寫操作先行發生於後面對這個變數的讀操作,這裡的 “後面” 同樣是指時間上的先後順序。
執行緒啟動規則(Thread Start Rule):Thread 物件的 start() 方法先行發生於此執行緒的每一個動作。
執行緒終止規則(Thread Termination Rule):執行緒中的所有操作都先行發生於對此執行緒的終止檢測,我們可以通過 Thread.join() 方法結束、Thread.isAlive() 的返回值等手段檢測到執行緒已經終止執行。
執行緒中斷規則(Thread Interruption Rule):對執行緒 interrupt() 方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生,可以通過 Thread.interrupted() 方法檢測到是否有中斷髮生。
物件終結規則(Finalizer Rule):一個物件的初始化完成(建構函式執行結束)先行發生於它的 finalize() 方法的開始。
傳遞性(Transitivity):如果操作 A 先行發生於操作 B,操作 B 先行發生於操作 C,那就可以得出操作 A 先行發生於操作 C 的結論。
時間先後順序與先行發生原則之間基本沒有太大的關係,所以我們衡量併發完全問題的時候不要受到時間順序的干擾,一切必須以先行發生原則為準。