隨著計(jì)算機(jī)系統(tǒng)的發(fā)展,多任務(wù)處理器系統(tǒng)在現(xiàn)代計(jì)算機(jī)操作系統(tǒng)中已經(jīng)是一個(gè)必不可少的組成部分了。在很多情況下,如果想要避免因計(jì)算機(jī)的運(yùn)算速度與它的存儲(chǔ)和通信子系統(tǒng)的差距太大而造成的磁盤(pán)I/O、網(wǎng)絡(luò)通信、數(shù)據(jù)庫(kù)訪問(wèn)時(shí)間太長(zhǎng)的問(wèn)題,就必須去“壓榨”處理器的運(yùn)算能力,讓計(jì)算機(jī)能夠同時(shí)處理多項(xiàng)任務(wù)。并發(fā)問(wèn)題的產(chǎn)生也引起了物理機(jī)及虛擬機(jī)內(nèi)存模型的變革。
1. 硬件的效率與一致性
在正式了解Java虛擬機(jī)并發(fā)相關(guān)知識(shí)之前,先了解下物理計(jì)算機(jī)之中的并發(fā)問(wèn)題,物理機(jī)對(duì)并發(fā)的處理方案對(duì)虛擬機(jī)的實(shí)現(xiàn)有相當(dāng)大的參考意義。
計(jì)算機(jī)多任務(wù)的執(zhí)行,都不可能只靠處理器的“運(yùn)算”而完成。其中,處理器至少要與內(nèi)存交互讀取運(yùn)算數(shù)據(jù)、存儲(chǔ)運(yùn)算結(jié)果,這個(gè)I/O操作是很難消除的。然而由于計(jì)算機(jī)存儲(chǔ)設(shè)備與處理器的運(yùn)算速度有幾個(gè)數(shù)量級(jí)的差距,所以現(xiàn)代計(jì)算機(jī)系統(tǒng)中都加入了一層讀寫(xiě)速度接近處理器運(yùn)算速度的高速緩存(Cache)來(lái)作為內(nèi)存與處理器之間的緩沖:將運(yùn)算要使用到的數(shù)據(jù)先復(fù)制到緩存中,讓運(yùn)算能夠更快速的執(zhí)行,當(dāng)運(yùn)算結(jié)束后再?gòu)木彺嬷型交貎?nèi)存之中,從而避免了緩慢的內(nèi)存讀寫(xiě)。同時(shí),這種設(shè)計(jì)又引入了一個(gè)新的問(wèn)題——緩存一致性問(wèn)題。在多處理器系統(tǒng)中,每個(gè)處理器都有自己的高速緩存,而同時(shí)又共享同一主內(nèi)存,當(dāng)多個(gè)處理器的運(yùn)算任務(wù)都涉及到同一塊內(nèi)存區(qū)域時(shí),將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致。為了解決一致性問(wèn)題就需要各個(gè)處理器在訪問(wèn)緩存時(shí)都遵循一些協(xié)議。內(nèi)存模型即在特定的操作協(xié)議下,對(duì)特定的內(nèi)存或高速緩存進(jìn)行讀寫(xiě)訪問(wèn)的過(guò)程抽象。

2. Java 內(nèi)存模型
Java內(nèi)存模型的主要目的是定義程序中各個(gè)變量的訪問(wèn)規(guī)則,即在虛擬機(jī)中j將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)。
-
主內(nèi)存與工作內(nèi)存
Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存(Main Memory)中,同時(shí)每條線程還有自己的工作內(nèi)存 (Working Memory),線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存副本拷貝,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫(xiě)主內(nèi)存中的變量。不同的線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過(guò)主內(nèi)存來(lái)完成。

-
內(nèi)存間交互操作
Java內(nèi)存模型中定義了以下8種操作來(lái)完成工作內(nèi)存與主內(nèi)存之間的交互。
- lock(鎖定) : 作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占的狀態(tài)。
- unlock(解鎖) : 作用于主內(nèi)存的變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才可以被其他線程鎖定。
- read(讀取) : 作用于主內(nèi)存的變量,把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用。
- load(載入) : 作用于工作內(nèi)存的變量,把read操作所得到的值放入工作內(nèi)存的變量副本中。
- use(使用) : 作用于工作內(nèi)存的變量,把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
- assign(賦值) :作用于工作內(nèi)存的變量,把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
- store(存儲(chǔ)) : 作用于工作內(nèi)存的變量,把工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write操作使用。
- write(寫(xiě)入) : 作用于主內(nèi)存的變量,把store操作所從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。
同時(shí),Java內(nèi)存模型還規(guī)定了在執(zhí)行上述8種操作時(shí)必須滿足如下規(guī)則:
- 不允許read和load、store和write操作之一單獨(dú)出現(xiàn),即不允許一個(gè)變量從主內(nèi)存讀取了但工作內(nèi)存不接受,或者從工作內(nèi)存發(fā)起回寫(xiě)了但主內(nèi)存不接受的情況。
- 不允許一個(gè)線程丟棄它的最近assign操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存。
- 不允許一個(gè)線程無(wú)原因的(沒(méi)有發(fā)生過(guò)任何assign操作)把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存中。
- 一個(gè)新的變量只能在主內(nèi)存中“誕生”,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量,就是對(duì)一個(gè)變量執(zhí)行use和store之前必須先執(zhí)行過(guò)了assign和load操作。
- 一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會(huì)被解鎖。
- 如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,僵尸清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值。
- 如果一個(gè)變量事先沒(méi)有被lock操作鎖定,則不允許對(duì)它執(zhí)行unlock操作,也不允許去unlock一個(gè)被其他線程鎖定住的變量。
- 對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store和write操作)。
-
volatile關(guān)鍵字
volatile關(guān)鍵字是Java虛擬機(jī)提供的最輕量級(jí)的同步機(jī)制,當(dāng)一個(gè)變量被定義成volatile后,它將具備兩種特性:
-
保證此變量對(duì)所有線程的可見(jiàn)性:
當(dāng)一條線程修改了這個(gè)變量的值,新值對(duì)于其他線程來(lái)說(shuō)是可以立即得知的。雖然volatile變量在各個(gè)線程中是一致的,但由于Java里面的運(yùn)算并非原子操作,而導(dǎo)致了volatile變量的運(yùn)算在并發(fā)下一樣是不安全的。
public static volatile int value = 0;
private static final int THREAD_COUNT = 20;
public static void increase(){
value ++;
}
public static void main(String[] args) {
Thread [] threads = new Thread[THREAD_COUNT];
for (int i = 0 ; i < THREAD_COUNT ; i++){
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0 ; i < 1000 ;i ++){
increase();
}
}
});
threads[i].start();
}
while (Thread.activeCount()>2)
// Thread.currentThread().getThreadGroup().list(); 獲取當(dāng)前項(xiàng)目所有線程并輸出
Thread.yield();//主線程讓出cpu使用權(quán)
System.out.println(value);
}
}
輸出結(jié)果 :
19372
通過(guò)Javap指令反編譯代碼后可以得到increase()方法的字節(jié)碼指令:
public static void increase();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field value:I
3: iconst_1
4: iadd
5: putstatic #2 // Field value:I
8: return
LineNumberTable:
line 20: 0
line 21: 8
從字節(jié)碼層面容易來(lái)分析并發(fā)失敗的原因:當(dāng)getstatic指令把value的值取到操作棧頂時(shí),volatile關(guān)鍵字保證了value的值在此時(shí)是正確的,但是在執(zhí)行iconst_1, iadd這些指令時(shí),其他線程可能已經(jīng)把race的值加大了,而操作棧頂?shù)闹稻妥兂闪诉^(guò)期的數(shù)據(jù),所以putstatic指令執(zhí)行后就可能把較小的value值同步回主內(nèi)存中。
由于volatile變量只能保證可見(jiàn)性,在不符合以下兩條規(guī)則的運(yùn)算場(chǎng)景中,我們?nèi)匀灰ㄟ^(guò)加鎖(使用synchronized或java.util.concurrent中的原子類)來(lái)保證原子性:
1)運(yùn)算結(jié)果并不依賴變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值。
2)變量不需要與其他的狀態(tài)變量共同參與不變約束。
-
禁止指令重排序優(yōu)化:
普通變量?jī)H僅會(huì)保證在該方法的執(zhí)行過(guò)程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致。因?yàn)樵谝粋€(gè)線程的方法執(zhí)行過(guò)程中無(wú)法感知到這點(diǎn),這也就是Java內(nèi)存模型中描述的所謂的“線程內(nèi)表現(xiàn)為串行的語(yǔ)義。
java內(nèi)存模型中對(duì)volatile變量定義的特殊規(guī)則:假定T表示一個(gè)線程,V和W分別表示volatile型變量,那么在進(jìn)行read、load、use、assign、store和write操作時(shí)需要滿足如下規(guī)則:
1)只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)作為load時(shí),T才能對(duì)V執(zhí)行use;并且,只有T對(duì)V執(zhí)行的后一個(gè)動(dòng)作為use時(shí),T才能對(duì)V執(zhí)行l(wèi)oad。T對(duì)V的use,可以認(rèn)為是和T對(duì)V的load。read動(dòng)作相關(guān)聯(lián),必須連續(xù)一起出現(xiàn)(這條規(guī)則要求在工作內(nèi)存中,每次使用V前都必須先從主內(nèi)存刷新最新的值,用于保證能看見(jiàn)其他線程對(duì)V修改后的值)。
2)只有當(dāng)T對(duì)V的前一個(gè)動(dòng)作是assign時(shí),T才能對(duì)V執(zhí)行store;并且,只有當(dāng)T對(duì)V執(zhí)行的后一個(gè)動(dòng)作是store時(shí),T才能對(duì)V執(zhí)行assign。T對(duì)V的assign可以認(rèn)為和T對(duì)V的store、write相關(guān)聯(lián),必須連續(xù)一起出現(xiàn)(這條規(guī)則要求在工作內(nèi)存中,每次修改V后都必須立刻同步回主內(nèi)存中,用于保證其他線程看到自己對(duì)V的修改)。
3)假定動(dòng)作A是T對(duì)V實(shí)施的use或assign動(dòng)作,假定動(dòng)作F是和動(dòng)作A相關(guān)聯(lián)的load或store動(dòng)作,假定動(dòng)作P是和動(dòng)作F相應(yīng)的對(duì)V的read或write動(dòng)作;類似的,假定動(dòng)作B是T對(duì)W實(shí)施的use或assign動(dòng)作,假定動(dòng)作G是和動(dòng)作B相關(guān)聯(lián)的load或store動(dòng)作,假定動(dòng)作Q是和動(dòng)作G相應(yīng)的對(duì)W的read或write動(dòng)作。如果A先于B,那么P先于Q(這條規(guī)則要求volatile修飾的變量不會(huì)被指令的重排序優(yōu)化,保證代碼的執(zhí)行順序與程序的順序相同)。
-
對(duì)long和double型變量的特殊規(guī)則
Java內(nèi)存模型允許虛擬機(jī)將沒(méi)有被volatile修飾的64位數(shù)據(jù)類型(long和double)的讀取操作劃分為兩次32位的操作來(lái)進(jìn)行,即允許虛擬機(jī)實(shí)現(xiàn)選擇可以不保證64位數(shù)據(jù)類型的load、store、read和write這4個(gè)操作的原子性,就點(diǎn)就是long和double的非原子協(xié)定(Nonatomic Treatment of double and long Variables)。
如果多個(gè)線程共享一個(gè)為聲明為volatile的long或double類型變量,并同時(shí)對(duì)他們進(jìn)行讀取和修改操作,那么有些線程可能會(huì)讀取到一個(gè)即非原值,也不是其他線程修改值得代表了“半個(gè)變量”的數(shù)值。
不過(guò)這種讀取帶“半個(gè)變量”的情況非常罕見(jiàn)(在目前商用虛擬機(jī)中不會(huì)出現(xiàn)),因?yàn)镴ava內(nèi)存模型雖然允許虛擬機(jī)不把long和double變量的讀寫(xiě)實(shí)現(xiàn)成原子操作,但允許虛擬機(jī)選擇把這些操作實(shí)現(xiàn)為具有原子性的操作,而且還“強(qiáng)烈建議”虛擬機(jī)這樣實(shí)現(xiàn)。因此我們?cè)诰帉?xiě)代碼時(shí)一般不需要把用到的long和double變量專門(mén)聲明為volatile。
-
原子性、可見(jiàn)性和有序性
原子性(Atomicity):由Java內(nèi)存模型來(lái)直接保證的原子性變量操作包括read、load、assign、use、store和write,我們大致可以認(rèn)為基本數(shù)據(jù)類型的訪問(wèn)具備原子性(long和double例外),同時(shí)我們也可以通過(guò)synchronized關(guān)鍵字來(lái)保證其中的操作具備原子性。
可見(jiàn)性(Visibility):指當(dāng)一個(gè)線程修改了共享變量的值,其他線程能夠立即得知這個(gè)修改。除了volatile,Java還有兩個(gè)關(guān)鍵字能實(shí)現(xiàn)可見(jiàn)性,synchronized和final。同步塊的可見(jiàn)性是由“對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須把此變量同步回主內(nèi)存中(執(zhí)行store和write操作)”這條規(guī)則獲得的,而final關(guān)鍵字的可見(jiàn)性是指:被final修飾的字段在構(gòu)造器中一旦被初始化完成,并且構(gòu)造器沒(méi)有把“this”的引用傳遞出去(this引用逃逸是一件很危險(xiǎn)的事情,其他線程有可能通過(guò)這個(gè)引用訪問(wèn)到“初始化了一半”的對(duì)象),那么其他線程中就能看見(jiàn)final字段的值。
有序性(Ordering):Java程序中天然的有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有的操作都是有序的;如果在一個(gè)線程中觀察另外一個(gè)線程,所有的操作都是無(wú)序的。前半句是指“線程內(nèi)表現(xiàn)為串行的語(yǔ)義”(Within-Thread As-if-Serial Semantics),后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。Java語(yǔ)言提供了volatile和synchronized兩個(gè)關(guān)鍵字來(lái)保證線程之間操作的有序性,volatile關(guān)鍵字本身就包含了禁止指令重排序的語(yǔ)義,而synchronized則是由“一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作”這條規(guī)則獲得的,這個(gè)規(guī)則決定了持有同一個(gè)鎖的兩個(gè)同步塊只能串行地進(jìn)入。
-
先行發(fā)生原則
先行發(fā)生是Java內(nèi)存模型中定義的兩項(xiàng)操作之間的偏序關(guān)系,如果操作A先行發(fā)生于操作B,其實(shí)就是說(shuō)在發(fā)生操作B之前,操作A產(chǎn)生的影響能被操作B觀察到,“影響”包括修改了內(nèi)存中共享變量的值/發(fā)送了消息/調(diào)用了方法等。如下是Java內(nèi)存模型下一些“天然的”先行發(fā)生關(guān)系,無(wú)須任何同步器協(xié)助就已經(jīng)存在,可直接在編碼中使用。如果兩個(gè)操作之間的關(guān)系不在此列,并且無(wú)法從下列規(guī)則推倒出來(lái),它們就沒(méi)有順序性的保障,虛擬機(jī)可以對(duì)它們進(jìn)行隨意地重排序。
1)程序次序規(guī)則(Program Order Rule):在一個(gè)線程內(nèi),按照程序代碼順序,書(shū)寫(xiě)在前面的操作先行發(fā)生于書(shū)寫(xiě)在后面的操作。準(zhǔn)確地來(lái)說(shuō)應(yīng)該是控制流順序而不是程序代碼順序,因?yàn)橐紤]分支/循環(huán)結(jié)構(gòu)。
2)管程鎖定規(guī)則(Monitor Lock Rule):一個(gè)unlock操作先行發(fā)生于后面對(duì)同一鎖的lock操作。這里必須強(qiáng)調(diào)的是同一鎖,而“后面”是指時(shí)間上的先后順序。
3)volatile變量規(guī)則(Volatile Variable Rule):對(duì)一個(gè)volatile變量的寫(xiě)操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作,這里的“后面”是指時(shí)間上的先后順序。
4)線程啟動(dòng)規(guī)則(Thread Start Rule):Thread對(duì)象的start()方法先行發(fā)生于此線程的每一個(gè)動(dòng)作。
5)線程終止規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對(duì)此線程的終止檢測(cè),我們可以通過(guò)Thread.join()方法結(jié)束/Thread.isAlive()的返回值等手段檢測(cè)到線程已經(jīng)終止執(zhí)行。
6)線程中斷規(guī)則(Thread Interruption Rule):對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生,可以通過(guò)Thread.interrupted()方法檢測(cè)到是否有中斷發(fā)生。
7)對(duì)象終結(jié)規(guī)則(Finalizer Rule):一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開(kāi)始。
8)傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那么操作A先行發(fā)生于操作C。
時(shí)間上的先后順序與先行發(fā)生原則之間基本沒(méi)有太大的關(guān)系,所以衡量并發(fā)安全問(wèn)題時(shí)不要受時(shí)間順序的干擾,一切必須以先行發(fā)生原則為準(zhǔn)。
有關(guān)happens-before規(guī)則的示例具體參考文章:Happens-Before規(guī)則 (先行發(fā)生原則)