Java內(nèi)存模型
Java內(nèi)存模型即Java Memory Model,簡(jiǎn)稱JMM。JMM定義了Java 虛擬機(jī)(JVM)在計(jì)算機(jī)內(nèi)存(RAM)中的工作方式。JVM是整個(gè)計(jì)算機(jī)虛擬模型,所以JMM是隸屬于JVM的。
如果我們要想深入了解Java并發(fā)編程,就要先理解好Java內(nèi)存模型。Java內(nèi)存模型定義了多線程之間共享變量的可見性以及如何在需要的時(shí)候?qū)蚕碜兞窟M(jìn)行同步。原始的Java內(nèi)存模型效率并不是很理想,因此Java1.5版本對(duì)其進(jìn)行了重構(gòu),現(xiàn)在的Java8仍沿用了Java1.5的版本。
JMM決定一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)另一個(gè)線程可見。從抽象的角度來(lái)看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存(main memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(local memory),本地內(nèi)存中存儲(chǔ)了該線程讀/寫共享變量的副本。本地內(nèi)存是JMM的一個(gè)抽象概念,并不真實(shí)存在。它涵蓋了緩存,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化。
從上圖來(lái)看,線程A與線程B之間如要通信的話,必須要經(jīng)歷下面2個(gè)步驟:
- 首先,線程A把本地內(nèi)存A中更新過(guò)的共享變量刷新到主內(nèi)存中去。
- 然后,線程B到主內(nèi)存中去讀取線程A之前已更新過(guò)的共享變量。
下面通過(guò)示意圖來(lái)說(shuō)明這兩個(gè)步驟:
如上圖所示,本地內(nèi)存A和B有主內(nèi)存中共享變量x的副本。假設(shè)初始時(shí),這三個(gè)內(nèi)存中的x值都為0。線程A在執(zhí)行時(shí),把更新后的x值(假設(shè)值為1)臨時(shí)存放在自己的本地內(nèi)存A中。當(dāng)線程A和線程B需要通信時(shí),線程A首先會(huì)把自己本地內(nèi)存中修改后的x值刷新到主內(nèi)存中,此時(shí)主內(nèi)存中的x值變?yōu)榱?。隨后,線程B到主內(nèi)存中去讀取線程A更新后的x值,此時(shí)線程B的本地內(nèi)存的x值也變?yōu)榱?。
從整體來(lái)看,這兩個(gè)步驟實(shí)質(zhì)上是線程A在向線程B發(fā)送消息,而且這個(gè)通信過(guò)程必須要經(jīng)過(guò)主內(nèi)存。JMM通過(guò)控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互,來(lái)為java程序員提供內(nèi)存可見性保證。
上面也說(shuō)到了,Java內(nèi)存模型只是一個(gè)抽象概念,那么它在Java中具體是怎么工作的呢?為了更好的理解Java內(nèi)存模型工作方式,下面就JVM對(duì)Java內(nèi)存模型的實(shí)現(xiàn)、硬件內(nèi)存模型及它們之間的橋接做詳細(xì)介紹。
JVM對(duì)Java內(nèi)存模型的實(shí)現(xiàn)
在JVM內(nèi)部,Java內(nèi)存模型把內(nèi)存分成了兩部分:線程棧區(qū)和堆區(qū),下圖展示了Java內(nèi)存模型在JVM中的邏輯視圖:
JVM中運(yùn)行的每個(gè)線程都擁有自己的線程棧,線程棧包含了當(dāng)前線程執(zhí)行的方法調(diào)用相關(guān)信息,我們也把它稱作調(diào)用棧。隨著代碼的不斷執(zhí)行,調(diào)用棧會(huì)不斷變化。
線程棧還包含了當(dāng)前方法的所有本地變量信息。一個(gè)線程只能讀取自己的線程棧,也就是說(shuō),線程中的本地變量對(duì)其它線程是不可見的。即使兩個(gè)線程執(zhí)行的是同一段代碼,它們也會(huì)各自在自己的線程棧中創(chuàng)建本地變量,因此,每個(gè)線程中的本地變量都會(huì)有自己的版本。
所有原始類型(boolean,byte,short,char,int,long,float,double)的本地變量都直接保存在線程棧當(dāng)中,對(duì)于它們的值各個(gè)線程之間都是獨(dú)立的。對(duì)于原始類型的本地變量,一個(gè)線程可以傳遞一個(gè)副本給另一個(gè)線程,當(dāng)它們之間是無(wú)法共享的。
堆區(qū)包含了Java應(yīng)用創(chuàng)建的所有對(duì)象信息,不管對(duì)象是哪個(gè)線程創(chuàng)建的,其中的對(duì)象包括原始類型的封裝類(如Byte、Integer、Long等等)。不管對(duì)象是屬于一個(gè)成員變量還是方法中的本地變量,它都會(huì)被存儲(chǔ)在堆區(qū)。
下圖展示了調(diào)用棧和本地變量都存儲(chǔ)在棧區(qū),對(duì)象都存儲(chǔ)在堆區(qū):
一個(gè)本地變量如果是原始類型,那么它會(huì)被完全存儲(chǔ)到棧區(qū)。
一個(gè)本地變量也有可能是一個(gè)對(duì)象的引用,這種情況下,這個(gè)本地引用會(huì)被存儲(chǔ)到棧中,但是對(duì)象本身仍然存儲(chǔ)在堆區(qū)。
對(duì)于一個(gè)對(duì)象的成員方法,這些方法中包含本地變量,仍需要存儲(chǔ)在棧區(qū),即使它們所屬的對(duì)象在堆區(qū)。
對(duì)于一個(gè)對(duì)象的成員變量,不管它是原始類型還是包裝類型,都會(huì)被存儲(chǔ)到堆區(qū)。
Static類型的變量以及類本身相關(guān)信息都會(huì)隨著類本身存儲(chǔ)在堆區(qū)。
堆中的對(duì)象可以被多線程共享。如果一個(gè)線程獲得一個(gè)對(duì)象的引用,它便可訪問這個(gè)對(duì)象的成員變量。如果兩個(gè)線程同時(shí)調(diào)用了同一個(gè)對(duì)象的同一個(gè)方法,那么這兩個(gè)線程便可同時(shí)訪問這個(gè)對(duì)象的成員變量,但是對(duì)于本地變量,每個(gè)線程都會(huì)拷貝一份到自己的線程棧中。
下圖展示了上面描述的過(guò)程:
兩個(gè)線程擁有一些本地變量。其中一個(gè)本地變量(Local Variable 2)執(zhí)行堆上的一個(gè)共享對(duì)象(Object 3)。這兩個(gè)線程分別擁有同一個(gè)對(duì)象的不同引用。這些引用都是本地變量,因此存放在各自線程的線程棧上。這兩個(gè)不同的引用指向堆上同一個(gè)對(duì)象。
注意,這個(gè)共享對(duì)象(Object 3)持有Object2和Object4一個(gè)引用作為其成員變量(如圖中Object3指向Object2和Object4的箭頭)。通過(guò)Object3中這些成員變量引用,這兩個(gè)線程就可以訪問Object2和Object4。
這張圖也展示了指向堆上兩個(gè)不同對(duì)象的一個(gè)本地變量。在這種情況下,指向兩個(gè)不同對(duì)象的引用不是同一個(gè)對(duì)象。理論上,兩個(gè)線程都可以訪問Object1和Object5,如果兩個(gè)線程都擁有兩個(gè)對(duì)象的引用。但是在上圖中,每一個(gè)線程僅有一個(gè)引用指向兩個(gè)對(duì)象其中之一。
因此,什么類型的Java代碼會(huì)導(dǎo)致上面的內(nèi)存圖呢?如下所示:
public class MyRunnable implements Runnable() {
public void run() {
methodOne();
}
public void methodOne() {
int localVariable1 = 45;
MySharedObject localVariable2 =
MySharedObject.sharedInstance;
//... do more with local variables.
methodTwo();
}
public void methodTwo() {
Integer localVariable1 = new Integer(99);
//... do more with local variable.
}
}
public class MySharedObject {
//static variable pointing to instance of MySharedObject
public static final MySharedObject sharedInstance =
new MySharedObject();
//member variables pointing to two objects on the heap
public Integer object2 = new Integer(22);
public Integer object4 = new Integer(44);
public long member1 = 12345;
public long member1 = 67890;
}
如果兩個(gè)線程同時(shí)執(zhí)行run()方法,就會(huì)出現(xiàn)上圖所示的情景。run()方法調(diào)用methodOne()方法,methodOne()調(diào)用methodTwo()方法。
methodOne()聲明了一個(gè)原始類型的本地變量和一個(gè)引用類型的本地變量。
每個(gè)線程執(zhí)行methodOne()都會(huì)在它們對(duì)應(yīng)的線程棧上創(chuàng)建localVariable1和localVariable2的私有拷貝。localVariable1變量彼此完全獨(dú)立,僅“生活”在每個(gè)線程的線程棧上。一個(gè)線程看不到另一個(gè)線程對(duì)它的localVariable1私有拷貝做出的修改。
每個(gè)線程執(zhí)行methodOne()時(shí)也將會(huì)創(chuàng)建它們各自的localVariable2拷貝。然而,兩個(gè)localVariable2的不同拷貝都指向堆上的同一個(gè)對(duì)象。代碼中通過(guò)一個(gè)靜態(tài)變量設(shè)置localVariable2指向一個(gè)對(duì)象引用。僅存在一個(gè)靜態(tài)變量的一份拷貝,這份拷貝存放在堆上。因此,localVariable2的兩份拷貝都指向由MySharedObject指向的靜態(tài)變量的同一個(gè)實(shí)例。MySharedObject實(shí)例也存放在堆上。它對(duì)應(yīng)于上圖中的Object3。
注意,MySharedObject類也包含兩個(gè)成員變量。這些成員變量隨著這個(gè)對(duì)象存放在堆上。這兩個(gè)成員變量指向另外兩個(gè)Integer對(duì)象。這些Integer對(duì)象對(duì)應(yīng)于上圖中的Object2和Object4.
注意,methodTwo()創(chuàng)建一個(gè)名為localVariable的本地變量。這個(gè)成員變量是一個(gè)指向一個(gè)Integer對(duì)象的對(duì)象引用。這個(gè)方法設(shè)置localVariable1引用指向一個(gè)新的Integer實(shí)例。在執(zhí)行methodTwo方法時(shí),localVariable1引用將會(huì)在每個(gè)線程中存放一份拷貝。這兩個(gè)Integer對(duì)象實(shí)例化將會(huì)被存儲(chǔ)堆上,但是每次執(zhí)行這個(gè)方法時(shí),這個(gè)方法都會(huì)創(chuàng)建一個(gè)新的Integer對(duì)象,兩個(gè)線程執(zhí)行這個(gè)方法將會(huì)創(chuàng)建兩個(gè)不同的Integer實(shí)例。methodTwo方法創(chuàng)建的Integer對(duì)象對(duì)應(yīng)于上圖中的Object1和Object5。
還有一點(diǎn),MySharedObject類中的兩個(gè)long類型的成員變量是原始類型的。因?yàn)?,這些變量是成員變量,所以它們?nèi)匀浑S著該對(duì)象存放在堆上,僅有本地變量存放在線程棧上。
硬件內(nèi)存架構(gòu)
不管是什么內(nèi)存模型,最終還是運(yùn)行在計(jì)算機(jī)硬件上的,所以我們有必要了解計(jì)算機(jī)硬件內(nèi)存架構(gòu),下圖就簡(jiǎn)單描述了當(dāng)代計(jì)算機(jī)硬件內(nèi)存架構(gòu):
現(xiàn)代計(jì)算機(jī)一般都有2個(gè)以上CPU,而且每個(gè)CPU還有可能包含多個(gè)核心。因此,如果我們的應(yīng)用是多線程的話,這些線程可能會(huì)在各個(gè)CPU核心中并行運(yùn)行。
在CPU內(nèi)部有一組CPU寄存器,也就是CPU的儲(chǔ)存器。CPU操作寄存器的速度要比操作計(jì)算機(jī)主存快的多。在主存和CPU寄存器之間還存在一個(gè)CPU緩存,CPU操作CPU緩存的速度快于主存但慢于CPU寄存器。某些CPU可能有多個(gè)緩存層(一級(jí)緩存和二級(jí)緩存)。計(jì)算機(jī)的主存也稱作RAM,所有的CPU都能夠訪問主存,而且主存比上面提到的緩存和寄存器大很多。
當(dāng)一個(gè)CPU需要訪問主存時(shí),會(huì)先讀取一部分主存數(shù)據(jù)到CPU緩存,進(jìn)而在讀取CPU緩存到寄存器。當(dāng)CPU需要寫數(shù)據(jù)到主存時(shí),同樣會(huì)先f(wàn)lush寄存器到CPU緩存,然后再在某些節(jié)點(diǎn)把緩存數(shù)據(jù)flush到主存。
Java內(nèi)存模型和硬件架構(gòu)之間的橋接
正如上面講到的,Java內(nèi)存模型和硬件內(nèi)存架構(gòu)并不一致。硬件內(nèi)存架構(gòu)中并沒有區(qū)分棧和堆,從硬件上看,不管是棧還是堆,大部分?jǐn)?shù)據(jù)都會(huì)存到主存中,當(dāng)然一部分棧和堆的數(shù)據(jù)也有可能會(huì)存到CPU寄存器中,如下圖所示,Java內(nèi)存模型和計(jì)算機(jī)硬件內(nèi)存架構(gòu)是一個(gè)交叉關(guān)系:
當(dāng)對(duì)象和變量存儲(chǔ)到計(jì)算機(jī)的各個(gè)內(nèi)存區(qū)域時(shí),必然會(huì)面臨一些問題,其中最主要的兩個(gè)問題是:
- 共享對(duì)象對(duì)各個(gè)線程的可見性
- 共享對(duì)象的競(jìng)爭(zhēng)現(xiàn)象
共享對(duì)象的可見性
當(dāng)多個(gè)線程同時(shí)操作同一個(gè)共享對(duì)象時(shí),如果沒有合理的使用volatile和synchronization關(guān)鍵字,一個(gè)線程對(duì)共享對(duì)象的更新有可能導(dǎo)致其它線程不可見。
想象一下我們的共享對(duì)象存儲(chǔ)在主存,一個(gè)CPU中的線程讀取主存數(shù)據(jù)到CPU緩存,然后對(duì)共享對(duì)象做了更改,但CPU緩存中的更改后的對(duì)象還沒有flush到主存,此時(shí)線程對(duì)共享對(duì)象的更改對(duì)其它CPU中的線程是不可見的。最終就是每個(gè)線程最終都會(huì)拷貝共享對(duì)象,而且拷貝的對(duì)象位于不同的CPU緩存中。
下圖展示了上面描述的過(guò)程。左邊CPU中運(yùn)行的線程從主存中拷貝共享對(duì)象obj到它的CPU緩存,把對(duì)象obj的count變量改為2。但這個(gè)變更對(duì)運(yùn)行在右邊CPU中的線程不可見,因?yàn)檫@個(gè)更改還沒有flush到主存中:
要解決共享對(duì)象可見性這個(gè)問題,我們可以使用java volatile關(guān)鍵字。 Java’s volatile keyword. volatile 關(guān)鍵字可以保證變量會(huì)直接從主存讀取,而對(duì)變量的更新也會(huì)直接寫到主存。volatile原理是基于CPU內(nèi)存屏障指令實(shí)現(xiàn)的,后面會(huì)講到。
競(jìng)爭(zhēng)現(xiàn)象
如果多個(gè)線程共享一個(gè)對(duì)象,如果它們同時(shí)修改這個(gè)共享對(duì)象,這就產(chǎn)生了競(jìng)爭(zhēng)現(xiàn)象。
如下圖所示,線程A和線程B共享一個(gè)對(duì)象obj。假設(shè)線程A從主存讀取Obj.count變量到自己的CPU緩存,同時(shí),線程B也讀取了Obj.count變量到它的CPU緩存,并且這兩個(gè)線程都對(duì)Obj.count做了加1操作。此時(shí),Obj.count加1操作被執(zhí)行了兩次,不過(guò)都在不同的CPU緩存中。
如果這兩個(gè)加1操作是串行執(zhí)行的,那么Obj.count變量便會(huì)在原始值上加2,最終主存中的Obj.count的值會(huì)是3。然而下圖中兩個(gè)加1操作是并行的,不管是線程A還是線程B先f(wàn)lush計(jì)算結(jié)果到主存,最終主存中的Obj.count只會(huì)增加1次變成2,盡管一共有兩次加1操作。
要解決上面的問題我們可以使用java synchronized代碼塊。synchronized代碼塊可以保證同一個(gè)時(shí)刻只能有一個(gè)線程進(jìn)入代碼競(jìng)爭(zhēng)區(qū),synchronized代碼塊也能保證代碼塊中所有變量都將會(huì)從主存中讀,當(dāng)線程退出代碼塊時(shí),對(duì)所有變量的更新將會(huì)flush到主存,不管這些變量是不是volatile類型的。
volatile和synchronized區(qū)別
首先需要理解線程安全的兩個(gè)方面:執(zhí)行控制和內(nèi)存可見。
執(zhí)行控制的目的是控制代碼執(zhí)行(順序)及是否可以并發(fā)執(zhí)行。
內(nèi)存可見控制的是線程執(zhí)行結(jié)果在內(nèi)存中對(duì)其它線程的可見性。根據(jù)Java內(nèi)存模型的實(shí)現(xiàn),線程在具體執(zhí)行時(shí),會(huì)先拷貝主存數(shù)據(jù)到線程本地(CPU緩存),操作完成后再把結(jié)果從線程本地刷到主存。
synchronized關(guān)鍵字解決的是執(zhí)行控制的問題,它會(huì)阻止其它線程獲取當(dāng)前對(duì)象的監(jiān)控鎖,這樣就使得當(dāng)前對(duì)象中被synchronized關(guān)鍵字保護(hù)的代碼塊無(wú)法被其它線程訪問,也就無(wú)法并發(fā)執(zhí)行。更重要的是,synchronized還會(huì)創(chuàng)建一個(gè)內(nèi)存屏障,內(nèi)存屏障指令保證了所有CPU操作結(jié)果都會(huì)直接刷到主存中,從而保證了操作的內(nèi)存可見性,同時(shí)也使得先獲得這個(gè)鎖的線程的所有操作,都happens-before于隨后獲得這個(gè)鎖的線程的操作。
volatile關(guān)鍵字解決的是內(nèi)存可見性的問題,會(huì)使得所有對(duì)volatile變量的讀寫都會(huì)直接刷到主存,即保證了變量的可見性。這樣就能滿足一些對(duì)變量可見性有要求而對(duì)讀取順序沒有要求的需求。
使用volatile關(guān)鍵字僅能實(shí)現(xiàn)對(duì)原始變量(如boolen、 short 、int 、long等)操作的原子性,但需要特別注意, volatile不能保證復(fù)合操作的原子性,即使只是i++,實(shí)際上也是由多個(gè)原子操作組成:read i; inc; write i,假如多個(gè)線程同時(shí)執(zhí)行i++,volatile只能保證他們操作的i是同一塊內(nèi)存,但依然可能出現(xiàn)寫入臟數(shù)據(jù)的情況。
在Java 5提供了原子數(shù)據(jù)類型atomic wrapper classes,對(duì)它們的increase之類的操作都是原子操作,不需要使用sychronized關(guān)鍵字。
對(duì)于volatile關(guān)鍵字,當(dāng)且僅當(dāng)滿足以下所有條件時(shí)可使用:
- 對(duì)變量的寫入操作不依賴變量的當(dāng)前值,或者你能確保只有單個(gè)線程更新變量的值。
- 該變量沒有包含在具有其他變量的不變式中。
volatile和synchronized的區(qū)別:
- volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的,需要從主存中讀??;
- synchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問該變量,其他線程被阻塞?。?/li>
- volatile僅能使用在變量級(jí)別;synchronized則可以使用在變量、方法、和類級(jí)別的;
- volatile僅能實(shí)現(xiàn)變量的修改可見性,不能保證原子性;而synchronized則可以保證變量的修改可見性和原子性;
- volatile不會(huì)造成線程的阻塞;synchronized可能會(huì)造成線程的阻塞;
- volatile標(biāo)記的變量不會(huì)被編譯器優(yōu)化;synchronized標(biāo)記的變量可以被編譯器優(yōu)化。
支撐Java內(nèi)存模型的基礎(chǔ)原理
指令重排序
在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器會(huì)對(duì)指令做重排序。但是,JMM確保在不同的編譯器和不同的處理器平臺(tái)之上,通過(guò)插入特定類型的Memory Barrier來(lái)禁止特定類型的編譯器重排序和處理器重排序,為上層提供一致的內(nèi)存可見性保證。
- 編譯器優(yōu)化重排序:編譯器在不改變單線程程序語(yǔ)義的前提下,可以重新安排語(yǔ)句的執(zhí)行順序。
- 指令級(jí)并行的重排序:如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
- 內(nèi)存系統(tǒng)的重排序:處理器使用緩存和讀寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。
數(shù)據(jù)依賴性
如果兩個(gè)操作訪問同一個(gè)變量,其中一個(gè)為寫操作,此時(shí)這兩個(gè)操作之間存在數(shù)據(jù)依賴性。
編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴性關(guān)系的兩個(gè)操作的執(zhí)行順序,即不會(huì)重排序。
as-if-serial
不管怎么重排序,單線程下的執(zhí)行結(jié)果不能被改變,編譯器、runtime和處理器都必須遵守as-if-serial語(yǔ)義。
內(nèi)存屏障(Memory Barrier )
上面講到了,通過(guò)內(nèi)存屏障可以禁止特定類型處理器的重排序,從而讓程序按我們預(yù)想的流程去執(zhí)行。內(nèi)存屏障,又稱內(nèi)存柵欄,是一個(gè)CPU指令,基本上它是一條這樣的指令:
- 保證特定操作的執(zhí)行順序。
- 影響某些數(shù)據(jù)(或則是某條指令的執(zhí)行結(jié)果)的內(nèi)存可見性。
編譯器和CPU能夠重排序指令,保證最終相同的結(jié)果,嘗試優(yōu)化性能。插入一條Memory Barrier會(huì)告訴編譯器和CPU:不管什么指令都不能和這條Memory Barrier指令重排序。
Memory Barrier所做的另外一件事是強(qiáng)制刷出各種CPU cache,如一個(gè)Write-Barrier(寫入屏障)將刷出所有在Barrier之前寫入 cache 的數(shù)據(jù),因此,任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本。
這和java有什么關(guān)系?上面java內(nèi)存模型中講到的volatile是基于Memory Barrier實(shí)現(xiàn)的。
如果一個(gè)變量是volatile修飾的,JMM會(huì)在寫入這個(gè)字段之后插進(jìn)一個(gè)Write-Barrier指令,并在讀這個(gè)字段之前插入一個(gè)Read-Barrier指令。這意味著,如果寫入一個(gè)volatile變量,就可以保證:
- 一個(gè)線程寫入變量a后,任何線程訪問該變量都會(huì)拿到最新值。
- 在寫入變量a之前的寫入操作,其更新的數(shù)據(jù)對(duì)于其他線程也是可見的。因?yàn)镸emory Barrier會(huì)刷出cache中的所有先前的寫入。
happens-before
從jdk5開始,java使用新的JSR-133內(nèi)存模型,基于happens-before的概念來(lái)闡述操作之間的內(nèi)存可見性。
在JMM中,如果一個(gè)操作的執(zhí)行結(jié)果需要對(duì)另一個(gè)操作可見,那么這兩個(gè)操作之間必須要存在happens-before關(guān)系,這兩個(gè)操作既可以在同一個(gè)線程,也可以在不同的兩個(gè)線程中。
與程序員密切相關(guān)的happens-before規(guī)則如下:
- 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before于該線程中任意的后續(xù)操作。
- 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖操作,happens-before于隨后對(duì)這個(gè)鎖的加鎖操作。
- volatile域規(guī)則:對(duì)一個(gè)volatile域的寫操作,happens-before于任意線程后續(xù)對(duì)這個(gè)volatile域的讀。
- 傳遞性規(guī)則:如果 A happens-before B,且 B happens-before C,那么A happens-before C。
注意:兩個(gè)操作之間具有happens-before關(guān)系,并不意味前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行!僅僅要求前一個(gè)操作的執(zhí)行結(jié)果,對(duì)于后一個(gè)操作是可見的,且前一個(gè)操作按順序排在后一個(gè)操作之前。