一垮耳、volatile關(guān)鍵字簡介
synchronized關(guān)鍵字是阻塞式同步乞榨,在線程競爭激烈的時(shí)候會(huì)逐漸由偏向鎖膨脹為重量級(jí)鎖。而volatile是JVM提供的最輕量級(jí)的同步機(jī)制因谎。JMM告訴我們各個(gè)線程會(huì)將共享變量從主內(nèi)存中拷貝到工作內(nèi)存按声,然后執(zhí)行引擎會(huì)基于工作內(nèi)存中的數(shù)據(jù)進(jìn)行操作處理菱阵。不過線程在工作內(nèi)存中進(jìn)行操作后將會(huì)何時(shí)寫入主內(nèi)存中拯坟?這個(gè)時(shí)機(jī)普通機(jī)制是沒有規(guī)定的晤碘。
volatile一般用于修飾會(huì)被不同線程訪問和修改的變量衍锚,而針對(duì)volatile修飾的變量給JVM給了規(guī)定:線程對(duì)volatile變量的修改會(huì)立刻被其他線程感知友题,即被volatile修飾的變量能夠保證每個(gè)線程能夠獲取該變量的最新值,這樣就不會(huì)出現(xiàn)數(shù)據(jù)臟讀的現(xiàn)象构拳,保證了數(shù)據(jù)的可見性咆爽。
volatile具有可見性和有序性
二、volatile實(shí)現(xiàn)原理
加入volatile關(guān)鍵字的代碼的class字節(jié)碼中會(huì)多出了一個(gè)lock前綴指令置森,lock指令相當(dāng)于一個(gè)內(nèi)存屏障斗埂,主要做了三件事:
- 重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置
- 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回系統(tǒng)內(nèi)存
- 這個(gè)寫回內(nèi)存的操作會(huì)使其他CPU里緩存的該內(nèi)存地址的數(shù)據(jù)無效,即新寫入的值對(duì)別的線程可見
經(jīng)過這一波操作后凫海,其他的線程發(fā)現(xiàn)自己工作內(nèi)存中的緩存失效后呛凶,就會(huì)從內(nèi)存中重新讀取該變量數(shù)據(jù),即保證了其他線程可以獲取當(dāng)前最新值行贪。即可以說volatile實(shí)現(xiàn)了緩存一致性協(xié)議:每個(gè)處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己的緩存的值是不是過期了漾稀,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改模闲,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無效狀態(tài),當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候崭捍,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里尸折。
三、volatile的happens-before關(guān)系和內(nèi)存語義分析
在之前JMM一文中對(duì)happens-before規(guī)則介紹殷蛇,有一條是:對(duì)一個(gè)volatile域的寫实夹,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀。
來看一段代碼
class VolatileExam{
private int a = 0;
private volatile boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
public void reader() {
if (flag) { //3
int i = a; //4
}
}
}
對(duì)volatile的happens-before分析:
- 線程1先執(zhí)行writer方法粒梦,然后線程2執(zhí)行reader方法亮航。
- 我們由happens-before規(guī)則推知,
2 happens-before 3
(volatile變量的寫happens-before于任意后續(xù)對(duì)volatile變量的讀) - 由傳遞性可以得知
1 happens-before 4
- 由happens-before規(guī)則:如果
A happens-before B
匀们,則A的執(zhí)行結(jié)果對(duì)B可見缴淋,且A的執(zhí)行順序先于B的執(zhí)行順序 - 那么2的執(zhí)行結(jié)果對(duì)3可見,也就是說線程1將flag修改為true泄朴,線程2能夠迅速感知
volatile的內(nèi)存語義分析:
- 如果線程1先進(jìn)行writer方法重抖,隨后線程2進(jìn)行reader方法。一開始的本地都是a和flag的初始化狀態(tài)
- 在線程1線程2的本地內(nèi)存里叼旋,線程1對(duì)初始值進(jìn)行了修改并寫入主內(nèi)存中仇哆,而在線程2的本地內(nèi)存里還是原來的值
- 由于volatile變量寫后,線程中本地內(nèi)存中共享變量就會(huì)置為失效狀態(tài)夫植,因此線程2需要再次從主內(nèi)存中讀取最新的共享變量值讹剔。從橫向看,線程1和線程2進(jìn)行了通信详民,線程1在寫volatile變量的時(shí)候告訴線程2:你的本地內(nèi)存中的值是舊的
- 線程2在讀取volatile變量的時(shí)候就被告知目前自己的本地值是舊的延欠,那線程2就只能去主內(nèi)存中去取最新值了
四、volatile內(nèi)存語義的具體實(shí)現(xiàn)
為了性能優(yōu)化沈跨,JMM在不改變正確語義的前提下由捎,會(huì)允許編譯器和處理器對(duì)指令序列進(jìn)行重排序,那如果想阻止重排序就得添加內(nèi)存屏障饿凛。
四個(gè)內(nèi)存屏障:
屏障類型 | 指令類型 | 說明 |
---|---|---|
LoadLoadBarriers | Load1狞玛;LoadLoad;Load2 | 確保Load1的數(shù)據(jù)的裝載先于Load2及所有后續(xù)裝載指令的裝載 |
StoreStoreBarriers | Store1涧窒;StoreStore心肪;Store2 | 確保Store1數(shù)據(jù)對(duì)其他處理器可見(刷新到內(nèi)存)先于Store2及所有后續(xù)存儲(chǔ)指令的存儲(chǔ) |
LoadStoreBarriers | Load1;LoadStore纠吴;Store2 | 確保Load1的數(shù)據(jù)的裝載先于Store2及所有后續(xù)存儲(chǔ)指令的存儲(chǔ) |
StoreLoadBarriers | Store1硬鞍;StoreLoad;Load2 | 確保Store1的數(shù)據(jù)對(duì)其他處理器可見(刷新到內(nèi)存)先于Load2及所有后續(xù)的裝載指令的裝載 |
為了實(shí)現(xiàn)volatile內(nèi)存語義,編譯器在生成字節(jié)碼時(shí)固该,會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序:
- 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障
- 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障
StoreStore屏障:禁止上面的普通寫和下面的volatile寫重排序锅减;
StoreLoad屏障:防止上面的volatile寫與下面可能有的volatile讀/寫重排序
LoadLoad屏障:禁止下面所有的普通讀操作和上面的volatile讀重排序
LoadStore屏障:禁止下面所有的普通寫操作和上面的volatile讀重排序
五、volatile如何保證內(nèi)存可見性
復(fù)習(xí)一下JMM的8種原子操作:
- lock(鎖定):作用于主內(nèi)存中的變量伐坏,把一個(gè)變量表示為一個(gè)線程獨(dú)占的狀態(tài)
- unlock(解鎖):作用于主內(nèi)存中的變量怔匣,把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定
- read(讀戎):作用于主內(nèi)存的變量劫狠,把一個(gè)變量的值從主內(nèi)存讀取到線程的工作內(nèi)存中拴疤,以便于后面的load操作
- 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è)變量的值傳送給主存中以便于后面的write操作
- write(寫入):作用于主內(nèi)存中的變量组橄,把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中
volatile規(guī)定:read、load罚随、use動(dòng)作必須連續(xù)出現(xiàn)玉工;assign、store淘菩、write動(dòng)作必須連續(xù)出現(xiàn)
所以volatile保證:每次讀取前必須先從主內(nèi)存刷新最新的值遵班,每次寫入后必須立即同步回主內(nèi)存當(dāng)中。即volatile關(guān)鍵字修飾的變量看到的隨時(shí)是自己的最新值潮改。
六狭郑、volatile的注意事項(xiàng)
public class VolatileExample {
private static volatile int counter = 0;
public static void main(String[] args) {
//開十個(gè)線程,讓他們每個(gè)都自增10000次汇在,理論上應(yīng)該得到10000
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++)
counter++;
}
});
thread.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter);
}
}
多次運(yùn)行看出翰萨,每次都得不到10000,這說明volatile并不能保證整體原子性糕殉。問題就是counter++并不是一個(gè)原子操作亩鬼,他包含了三個(gè)步驟:
- 讀取counter的值
- 對(duì)counter+1
- 將新的值賦給變量counter
這么看來如果線程1讀取counter到工作內(nèi)存后,其他線程對(duì)這個(gè)值已經(jīng)做了自增操作阿蝶,那么線程A的這個(gè)值自然就是一個(gè)過期的值雳锋,造成了數(shù)據(jù)的臟讀,因此結(jié)果必然小于10000赡磅。
如果想讓volatile保證整體原子性魄缚,必須符合:
- 運(yùn)算結(jié)果不依賴變量的當(dāng)前值,或者能夠確保只有一個(gè)線程修改變量的值;
- 變量不需要與其他的狀態(tài)變量共同參與不變約束
如果編譯器經(jīng)過分析后冶匹,認(rèn)定一個(gè)volatile變量只會(huì)被單個(gè)線程訪問习劫,那么編譯器可以把這個(gè)volatile變量當(dāng)做一個(gè)普通的變量來對(duì)待。