在上篇《單例,真了解嗎?》中,提及到了重排序、volatile等概念,有的同學(xué)還沒接觸過這些,那么此篇就簡單介紹一下它們。
JMM
java中,所有實例域、靜態(tài)域、數(shù)組元素都存儲在堆中,堆可被多個線程共享。在多線程環(huán)境下,線程之間是如何通信和實現(xiàn)這些共享數(shù)據(jù)的同步?java的并發(fā)采用的是共享內(nèi)存模型。
JMM(Java Memory Model),Java內(nèi)存模型,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。JMM定義了線程和主內(nèi)存之間的抽象關(guān)系,多個線程的共享數(shù)據(jù)存在主內(nèi)存中,但是每個線程擁有自己的本地內(nèi)存,本地內(nèi)存中存儲的是該線程從主內(nèi)存那里讀取共享變量的副本(注意:本地內(nèi)存是個抽象概念,不存在的),下圖是JMM的抽象結(jié)構(gòu)示意圖:

從圖來看,如果線程A要和線程B通信的話,需要兩步:
①線程A把本地內(nèi)存A中的更新過的共享變量刷新到主內(nèi)存中;
②線程B再從主內(nèi)存中讀取線程A之前已更新過的共享變量。
happens-before
在JMM中,如果一個操作執(zhí)行的結(jié)果需要對另一個操作可見,那么這兩個操作之間就必須存在happens-before關(guān)系,當(dāng)然兩個操作可能屬于同一線程,也有可能屬于不同的線程。
happens-before規(guī)則:
1、程序次序規(guī)則:在一個單獨的線程中,按照程序代碼的執(zhí)行流順序,(時間上)先執(zhí)行的操作happens-before(時間上)后執(zhí)行的操作。
2、管理鎖定規(guī)則:一個unlock操作happens-before后面(時間上的先后順序,下同)對同一個鎖的lock操作。
3、volatile變量規(guī)則:對一個volatile變量的寫操作happens-before后面對該變量的讀操作。
4、傳遞性:如果操作A happens-before操作B,操作B happens-before操作C,那么可以得出A happens-before操作C。
注意:兩個操作之間具有happens-before關(guān)系,并不意味前一個操作必須要在后一個操作之前執(zhí)行!僅僅要求前一個操作的執(zhí)行結(jié)果,對于后一個操作是可見的,且前一個操作按順序排在后一個操作之前。
重排序
重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序的一種方式。
數(shù)據(jù)依賴性
如果兩個操作訪問同一個變量,一個操作為寫操作,另一個是讀操作,那這兩個操作就存在數(shù)據(jù)依賴性。
//寫a
int a = 1;
//讀a
int b = a;
編譯器和處理器是不會對存在數(shù)據(jù)依賴關(guān)系的兩個操作做重排序的。(這里的數(shù)據(jù)依賴性僅針對單個處理器單線程中的操作,不同處理器不同線程間的數(shù)據(jù)依賴性不被編譯器和處理器考慮)
as-if-serial
as-if-serial的意思是,不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能被改變。編譯器和處理器都必須遵守as-if-serial語義。
//寫a
int a = 1; //A
//寫b
int b = 1; //B
//寫c
int c = a + b; //C
比如上面的這個例子,A和B之間沒有數(shù)據(jù)依賴性,A和C、B和C存在數(shù)據(jù)依賴關(guān)系,所以C不允許重排序到A和B前面(一旦排到A或B前面,執(zhí)行結(jié)果必定發(fā)生改變),A和B之間愛怎么排怎么排,對于執(zhí)行結(jié)果并無影響。
重排序?qū)Χ嗑€程的影響
public class Test8 {
int a = 0;
boolean flag = false;
public void writer(){
a = 1; //A
flag = true; //B
}
public void reader(){
if(flag){ //C
int i = a * a; //D
}
}
}
假如有兩個線程E和F,E先執(zhí)行writer()方法,然后F執(zhí)行reader()方法。最終線程F執(zhí)行完會得到始終一直的i嗎?是不行的。因為操作A和操作B沒有數(shù)據(jù)依賴性,A和B可能發(fā)生重排序,操作C和操作D也沒有數(shù)據(jù)依賴性,C和D也可能發(fā)生重排序。假設(shè)操作A和B發(fā)生了重排序,會怎樣?
程序時序圖如下:

當(dāng)操作A和操作B發(fā)生重排序,線程E首先標(biāo)記變量flag,隨后線程F讀flag,flag為真再讀取a。此時,變量a并未被線程E寫入,多線程語義就被重排序破壞掉了。
內(nèi)存屏障
內(nèi)存屏障,又稱內(nèi)存柵欄,是一個CPU指令,基本上它是一條這樣的指令:
1、保證特定操作的執(zhí)行順序。
2、影響某些數(shù)據(jù)(或則是某條指令的執(zhí)行結(jié)果)的內(nèi)存可見性。
編譯器和CPU能夠重排序指令,保證最終相同的結(jié)果,嘗試優(yōu)化性能。插入一條Memory Barrier會告訴編譯器和CPU:不管什么指令都不能和這條Memory Barrier指令重排序。
Memory Barrier所做的另外一件事是強制刷出各種CPU cache,如一個 Write-Barrier(寫入屏障)將刷出所有在 Barrier 之前寫入 cache 的數(shù)據(jù),因此,任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本。
volatile
volatile是基于Memory Barrier實現(xiàn)的,可以將volatile簡單看作是一個輕量級鎖。
public class Test8 {
//使用volatile修飾vl
volatile long vl = 0L;
public long getVl() {
return vl;
}
public void setVl(long vl) {
this.vl = vl;
}
public void getAndIncrement(){
vl++;
}
}
假如有多個線程分別調(diào)用上面程序的三個方法,程序在語義上和下面程序等價。
public class Test8 {
long vl = 0L;
public synchronized long getVl() {
return vl;
}
public synchronized void setVl(long vl) {
this.vl = vl;
}
public void getAndIncrement(){
long temp = getVl();
temp += 1L;
setVl(temp);
}
}
一個volatile變量的單個讀/寫操作,與一個普通變量的讀/寫操作都使用同一個鎖來同步效果一樣。鎖的happens-before規(guī)則保證釋放鎖和獲取鎖的兩個線程間的內(nèi)存可見性,所以對一個volatile變量的讀,總能看到任意線程對這個volatile變量最后的寫入。
簡單來說,volatile擁有以下特性;
①可見性,對一個volatile變量的讀,總能看到任意線程對這個volatile變量最后的寫入。
②不保證原子性,對任意單個volatile變量的讀/寫具有原子性,但對于像volatile++這種復(fù)合操作不具有原子性。
如果一個變量是volatile修飾的,JMM會在寫入這個字段之后插進一個Write-Barrier指令,并在讀這個字段之前插入一個Read-Barrier指令。
當(dāng)寫一個volatile變量時,JMM會把該線程對應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存中。
當(dāng)讀一個volatile變量時,JMM會把該線程對應(yīng)的本地內(nèi)存置為無效,然后再從主內(nèi)存中讀取共享變量。
問題:volatile為什么對復(fù)合操作不保證原子性?
答:拿上面的案例來講,某個時刻變量vl為10,線程1要調(diào)用getAndIncrement(),線程1先讀取vl的原始值,然后線程1被阻塞了;然后線程2也要調(diào)用getAndIncrement(),線程2也會先讀取vl的原始值,由于線程1直對vl進行了讀操作,并未做修改,所以不會線程2本地內(nèi)存中的緩存變量vl失效,不會導(dǎo)致主內(nèi)存的vl刷新,所以線程2讀取的vl還是10,調(diào)用完getAndIncrement()后,把11寫入本地內(nèi)存,再寫入主內(nèi)存。接著,線程1也調(diào)用了getAndIncrement(),由于已經(jīng)讀取了vl,線程1本地內(nèi)存的vl依然為10,自增完后vl變?yōu)閷ο?1,然后將11寫入本地內(nèi)存,最后寫入主內(nèi)存。2個線程一共執(zhí)行了兩次自增操作,但是最后vl是11不是12。
問題:對于volatile不能保證原子性,如何解決?
答:用synchronized或者Lock來加鎖。
問題:volatile和synchronized的區(qū)別有哪些?
答:①volatile只能修飾變量,而synchronized可修飾變量、方法、代碼塊;②volatile在多線程中不存在阻塞問題,synchronized存在阻塞問題;③volatile能保證數(shù)據(jù)的可見性,不能保證數(shù)據(jù)的原子性,synchronized能保證可見性、原子性;④volatile解決的是變量在線程間的可見性,而synchronized解決的是線程之間訪問資源的同步性。
總結(jié)
此篇主要介紹了JMM、happens-before、重排序、內(nèi)存屏障、volatile的一些基本概念及重要性質(zhì)。只有真正理解線程之間的通信機制,才能寫出健壯的并發(fā)程序。
本篇頂多算個基礎(chǔ)介紹,需要深入了解的同學(xué)可以閱讀書籍《并發(fā)編程的藝術(shù)》、《并發(fā)編程實戰(zhàn)》。
略陳固陋,如有不當(dāng)之處,歡迎各位看官批評指正!