1.并發(fā)編程中的三個概念
在并發(fā)編程中,我們通常會遇到以下三個問題:原子性問題,可見性問題,有序性問題厂镇。我們先看具體看一下這三個概念:
原子性:即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行左刽。
可見性:是指當(dāng)多個線程訪問同一個變量時捺信,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值悠反。
//線程1執(zhí)行的代碼
int i = 0;
i = 10;
//線程2執(zhí)行的代碼
j = I;
假若執(zhí)行線程1的是CPU1残黑,執(zhí)行線程2的是CPU2馍佑。由上面的分析可知,當(dāng)線程1執(zhí)行 i =10這句時梨水,會先把i的初始值加載到CPU1的高速緩存中拭荤,然后賦值為10,那么在CPU1的高速緩存當(dāng)中i的值變?yōu)?0了疫诽,卻沒有立即寫入到主存當(dāng)中舅世。此時線程2執(zhí)行 j = i,它會先去主存讀取i的值并加載到CPU2的緩存當(dāng)中奇徒,注意此時內(nèi)存當(dāng)中i的值還是0雏亚,那么就會使得j的值為0,而不是10.
這就是可見性問題摩钙,線程1對變量i修改了之后罢低,線程2沒有立即看到線程1修改的值。
有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行胖笛。
//線程1:
context = loadContext(); //語句1
inited = true; //語句2
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代碼中网持,由于語句1和語句2沒有數(shù)據(jù)依賴性,因此可能會被重排序长踊。假如發(fā)生了重排序功舀,在線程1執(zhí)行過程中先執(zhí)行語句2,而此是線程2會以為初始化工作已經(jīng)完成身弊,那么就會跳出while循環(huán)辟汰,去執(zhí)行doSomethingwithconfig(context)方法,而此時context并沒有被初始化阱佛,就會導(dǎo)致程序出錯帖汞。
從上面可以看出,指令重排序不會影響單個線程的執(zhí)行瘫絮,但是會影響到線程并發(fā)執(zhí)行的正確性涨冀。也就是說,要想并發(fā)程序正確地執(zhí)行麦萤,必須要保證原子性、可見性以及有序性扁眯。只要有一個沒有被保證壮莹,就有可能會導(dǎo)致程序運行不正確。
2.volatile關(guān)鍵字的兩層語義
一旦一個共享變量(類的成員變量姻檀、類的靜態(tài)成員變量)被volatile修飾之后命满,那么就具備了兩層語義:
- 保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值绣版,這新值對其他線程來說是立即可見的胶台。
- 禁止進行指令重排序歼疮。
假如線程1先執(zhí)行,線程2后執(zhí)行:
//線程1
boolean stop = false;
while(!stop){
doSomething();
}
//線程2
stop = true;
使用volatile修飾之后:
1诈唬、使用volatile關(guān)鍵字會強制將修改的值立即寫入主存韩脏;
2、使用volatile關(guān)鍵字的話铸磅,當(dāng)線程2進行修改時赡矢,會導(dǎo)致線程1的工作內(nèi)存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應(yīng)的緩存行無效)阅仔;
3吹散、由于線程1的工作內(nèi)存中緩存變量stop的緩存行無效,所以線程1再次讀取變量stop的值時會去主存讀取八酒。
那么在線程2修改stop值時(當(dāng)然這里包括2個操作空民,修改線程2工作內(nèi)存中的值,然后將修改后的值寫入內(nèi)存)羞迷,會使得線程1的工作內(nèi)存中緩存變量stop的緩存行無效界轩,然后線程1讀取時,發(fā)現(xiàn)自己的緩存行無效闭树,它會等待緩存行對應(yīng)的主存地址被更新之后耸棒,然后去對應(yīng)的主存讀取最新的值柒啤。線程1讀取到的就是最新的正確的值婚温。
3.volatile保證內(nèi)存可見性
對于可見性,Java提供了volatile關(guān)鍵字來保證可見性眼溶。
當(dāng)一個共享變量被volatile修飾時碍现,它會保證修改的值會立即被更新到主存幅疼,當(dāng)有其他線程需要讀取時,它會去內(nèi)存中讀取新值昼接。
而普通的共享變量不能保證可見性爽篷,因為普通共享變量被修改之后,什么時候被寫入主存是不確定的慢睡,當(dāng)其他線程去讀取時逐工,此時內(nèi)存中可能還是原來的舊值,因此無法保證可見性漂辐。
另外泪喊,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執(zhí)行同步代碼髓涯,并且在釋放鎖之前會將對變量的修改刷新到主存當(dāng)中袒啼。因此可以保證可見性。
4.volatile禁止指令重排
volatile關(guān)鍵字提供內(nèi)存屏障的方式來防止指令被重排,編譯器在生成字節(jié)碼文件時蚓再,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序滑肉。
在Java內(nèi)存模型中說過,為了性能優(yōu)化摘仅,編譯器和處理器會進行指令重排序靶庙;也就是說java程序天然的有序性可以總結(jié)為:如果在本線程內(nèi)觀察,所有的操作都是有序的实檀;如果在一個線程觀察另一個線程惶洲,所有的操作都是無序的。在單例模式的實現(xiàn)上有一種雙重檢驗鎖定的方式(Double-checked Locking)膳犹。代碼如下:
public class Singleton {
private Singleton() { }
private volatile static Singleton instance;
public Singleton getInstance(){
if(instance==null){
synchronized (Singleton.class){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
這里為什么要加volatile了恬吕?我們先來分析一下不加volatile的情況,有問題的語句是這條:instance = new Singleton();
這條語句實際上包含了三個操作:
- 1.分配對象的內(nèi)存空間须床;
- 2.初始化對象铐料;
- 3.設(shè)置instance指向剛分配的內(nèi)存地址。但由于存在重排序的問題豺旬,可能有以下的執(zhí)行順序:
如果2和3進行了重排序的話钠惩,線程B進行判斷if(instance==null)時就會為true,而實際上這個instance并沒有初始化成功族阅,顯而易見對線程B來說之后的操作就會是錯的篓跛。而用volatile修飾的話就可以禁止2和3操作重排序,從而避免這種情況坦刀。volatile包含禁止指令重排序的語義愧沟,其具有有序性。
通過生成匯編代碼鲤遥,可以清晰的看到加入volatile和未加入volatile的差別沐寺。volatile變量修飾的共享變量,在進行寫操作的時候會多出一個lock前綴的匯編指令盖奈,這個指令會觸發(fā)總線鎖或者緩存鎖混坞,通過緩存一致性協(xié)議來解決可見性問題。(可從Java內(nèi)存模型簡介了解緩存一致性協(xié)議)
0x01a3de1d:movb $0x0,0x1104800(%esi) ; ...c6860048 100100
0x01a3de24:lock addl $0x0,(%esp) ; ...f0830424 00
再比如下邊的例子
5.volatile如何保證有序性
在分析保證有序性前钢坦,有必要了解一下內(nèi)存屏障究孕。內(nèi)存屏障(Memory Barriers,Intel稱之Memory Fence)指令是指爹凹,重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置蚊俺。CPU把內(nèi)存屏障分成三類:寫屏障(store barrier)、讀屏障(load barrier)和全屏障(Full Barrier)逛万。
寫屏障store barrier相當(dāng)于storestore barrier, 強制所有在storestore內(nèi)存屏障之前的所有執(zhí)行,都要在該內(nèi)存屏障之前執(zhí)行,并發(fā)送緩存失效的信號宇植。所有在storestore barrier指令之后的store指令得封,都必須在storestore barrier屏障之前的指令執(zhí)行完后再被執(zhí)行。
讀屏障load barrier相當(dāng)于loadload barrier指郁,強制所有在load barrier讀屏障之后的load指令忙上,都在loadbarrier屏障之后執(zhí)行。
全屏障full barrier相當(dāng)于storeload闲坎,是一個全能型的屏障疫粥,因為它同時具備前面兩種屏障的效果。強制了所有在storeload barrier之前的store/load指令腰懂,都在該屏障之前被執(zhí)行梗逮,所有在該屏障之后的的store/load指令,都在該屏障之后被執(zhí)行绣溜。
在JMM中把內(nèi)存屏障指令分為4類:
- LoadLoad Barriers慷彤,load1 ; LoadLoad; load2 ,確保load1數(shù)據(jù)的裝載優(yōu)先于load2及所有后續(xù)裝載指令的裝載怖喻。
- StoreStore Barriers底哗,store1; storestore;store2 ,確保store1數(shù)據(jù)對其他處理器可見優(yōu)先于store2及所有后續(xù)存儲指令的存儲锚沸。
- LoadStore Barries跋选, load1;loadstore;store2,確保load1數(shù)據(jù)裝載優(yōu)先于store2以及后續(xù)的存儲指令刷新到內(nèi)存哗蜈。
- StoreLoad Barries前标, store1; storeload;load2, 確保store1數(shù)據(jù)對其他處理器變得可見恬叹, 優(yōu)先于load2及所有后續(xù)裝載指令的裝載候生;這條內(nèi)存屏障指令是一個全能型的屏障同時具有其他3條屏障的效果。
編譯器在生成字節(jié)碼時绽昼,會在volatile指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序唯鸭。對于編譯器來說,發(fā)現(xiàn)一個最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能硅确。為此目溉,JMM采取保守策略。下面是基于保守策略的JMM內(nèi)存屏障插入策略菱农。
- 在每個volatile寫操作的前面插入一個StoreStore屏障缭付。
- 在每個volatile寫操作的后面插入一個StoreLoad屏障。
- 在每個volatile讀操作的前面插入一個LoadLoad屏障循未。
- 在每個volatile讀操作的后面插入一個LoadStore屏障陷猫。
6.volatile無法保證原子性
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println(test.inc);
}
}
自增操作不是原子性操作,而且volatile也無法保證對變量的任何操作都是原子性的。
把上面的代碼改成以下任何一種都可以達到效果:
(1)采用synchronized
public class Test {
public int inc = 0;
public synchronized void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println(test.inc);
}
}
(2)采用Lock:
public class Test {
public int inc = 0;
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
try {
inc++;
} finally{
lock.unlock();
}
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println(test.inc);
}
}
7.volatile的原理和實現(xiàn)機制
前面講述了源于volatile關(guān)鍵字的一些使用绣檬,下面我們來探討一下volatile到底如何保證可見性和禁止指令重排序的足陨。
“觀察加入volatile關(guān)鍵字和沒有加入volatile關(guān)鍵字時所生成的匯編代碼發(fā)現(xiàn),加入volatile關(guān)鍵字時娇未,會多出一個lock前綴指令”lock前綴指令實際上相當(dāng)于一個內(nèi)存屏障(也成內(nèi)存柵欄)墨缘,內(nèi)存屏障會提供3個功能:
- 它確保指令重排序時不會把其后面的指令排到內(nèi)存屏障之前的位置,也不會把前面的指令排到內(nèi)存屏障的后面零抬;即在執(zhí)行到內(nèi)存屏障這句指令時镊讼,在它前面的操作已經(jīng)全部完成;
- 它會強制將對緩存的修改操作立即寫入主存平夜;
- 如果是寫操作蝶棋,它會導(dǎo)致其他CPU中對應(yīng)的緩存行無效。
8.volatile適用場景
-
synchronized關(guān)鍵字是防止多個線程同時執(zhí)行一段代碼褥芒,那么就會很影響程序執(zhí)行效率嚼松,而volatile關(guān)鍵字在某些情況下性能要優(yōu)于synchronized,是一種比synchronized 關(guān)鍵字更輕量級的同步機制锰扶。但是要注意volatile關(guān)鍵字是無法替代synchronized關(guān)鍵字的献酗,因為volatile關(guān)鍵字無法保證操作的原子性。通常來說坷牛,使用volatile必須具備以下2個條件:
- 對變量的寫操作不依賴于當(dāng)前值
- 該變量沒有包含在具有其他變量的不變式中
實際上罕偎,這些條件表明,可以被寫入 volatile 變量的這些有效值獨立于任何程序的狀態(tài)京闰,包括變量的當(dāng)前狀態(tài)颜及。上面的2個條件需要保證操作是原子性操作,才能保證使用volatile關(guān)鍵字的程序在并發(fā)時能夠正確執(zhí)行蹂楣。
9.思維導(dǎo)圖如下:
詳情介紹:
420天以來俏站,Java架構(gòu)更新了 888個主題,已經(jīng)有156+位同學(xué)加入痊土。微信掃碼關(guān)注java架構(gòu)肄扎,獲取Java面試題和架構(gòu)師相關(guān)題目和視頻。上述相關(guān)面試題答案赁酝,盡在Java架構(gòu)中犯祠。