1. 程式人生 > >Java內存模型與線程 深入理解Java虛擬機總結

Java內存模型與線程 深入理解Java虛擬機總結

物理機 指令重排 join 場景 blog 地方 原子操作 規則 sem

在許多情況下,讓計算機同時去做幾件事情,不僅是因為計算機的運算能力強大了,還有一個很重要的原因是計算機的運算速度與它的存儲和通信子系統速度的差距太大, 大量的時間都花費在磁盤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 的結論。

? ??????時間先後順序與先行發生原則之間基本沒有太大的關系,所以我們衡量並發完全問題的時候不要受到時間順序的幹擾,一切必須以先行發生原則為準。

Java內存模型與線程 深入理解Java虛擬機總結