寫在前面
前面一章我們講了了java原子性的相關(guān)概念和知識點(diǎn),介紹了用于共享變量線程隔離的ThreadLocal
分扎,也知道了synchronized
是一個重量級的鎖笼蛛,而我們今天要講的volatile則是輕量級的synchronized奴艾,主要是因?yàn)樗粫鹁€程上下文的切換橄妆。在講volatile
到底是什么,它能夠解決什么樣的問題之前符匾,首先不得不提一下Java的內(nèi)存模型叨咖。
JMM內(nèi)存模型
在Java中,所有實(shí)例域待讳,靜態(tài)域和數(shù)組對象都存在于堆中芒澜,是所有線程共享的。局部變量创淡,方法參數(shù)和異常處理參數(shù)不是線程共享痴晦,不會存在內(nèi)存可見性問題。
Java線程之間的通信由Java(簡稱JMM)內(nèi)存模型來控制琳彩,它決定了對一個變量的寫入何時對另一個線程可見誊酌。JMM
規(guī)定了所有的共享變量都存在于主內(nèi)存中,而每條線程又有自己的工作內(nèi)存露乏,工作內(nèi)存內(nèi)保存了對于主內(nèi)存中共享變量的拷貝碧浊。我們對一個變量的讀寫是在工作內(nèi)存中完成的,同時線程間的通信是由工作內(nèi)存將修改的變量刷新到主內(nèi)存來進(jìn)行傳遞的瘟仿。下面是JMM
的抽象示意圖:
從圖上看箱锐,線程A和B進(jìn)行通信的話分成兩步:
- 線程A將修改后的共享變量刷新到主內(nèi)存
- 線程B從主內(nèi)存讀取線程A已更新過的共享變量
由于線程B每次都是從主內(nèi)存拿變量,并不能實(shí)時地獲取線程A修改的變量值劳较,可能讀取的是之前的值驹止,從而出現(xiàn)臟讀浩聋,這就是不滿足可見性的問題。
可見性問題
可見性是指當(dāng)多個線程訪問同一個共享變量時臊恋,一個線程修改了這個變量的值衣洁,另一個線程立馬能夠讀取到修改后的值。
舉個栗子:
//線程1
int i = 0;
i = 10;
//線程2
j=i;
畫個圖:
由圖可知抖仅,假如線程A,B按照這種時間順序執(zhí)行的話坊夫,j最后的值是0。這就是可見性問題撤卢,線程A對變量i修改的值环凿,沒有立即對線程B可見。
有序性問題
有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行凸丸。
舉個栗子:
public class VolatileDemo {
private static boolean flag;
private static int num;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (!flag) {
Thread.yield();
}
System.out.println(num);
});
t1.start();
num = 5;
flag = true;
}
}
這段代碼拷邢,盡管num=5
是寫在flag=true
前面,但是最終打印的結(jié)果有可能是0哦屎慢。也就是說,寫在前面的代碼并沒有先執(zhí)行忽洛,對于這種不按書寫順序執(zhí)行的情況稱作指令重排序
腻惠。大多數(shù)現(xiàn)代處理器都支持指令重排,為的是直接運(yùn)行當(dāng)前能運(yùn)行的指令欲虚,而不去順序等待集灌,這種亂序的執(zhí)行方式打打提高了處理器的效率。
戲說不是胡說复哆,改編不是亂編欣喧,指令重排也不是隨便排,它是根據(jù)代碼的依賴關(guān)系梯找,在不影響單線程環(huán)境下的執(zhí)行結(jié)果的前提下進(jìn)行重排序的唆阿。例如:
a=1;
b=2;
c=a+b;
這段代碼,c=a+b
是不會重排到啊a,b之前的锈锤,因?yàn)閏的值對a,b都有依賴驯鳖。但是,a,b的賦值語句可能會重排久免。這種指令重排在單線程環(huán)境中沒有任何問題浅辙,但是在多線程的環(huán)境下,就將會出現(xiàn)數(shù)據(jù)的不確定性阎姥。
volatile保證可見性和有序性记舆,但不能保證原子性
在Java中,大佬們給我們提供了volatile
關(guān)鍵字來保證可見性和有序性呼巴。這個說法有兩種語義:
保證了不同線程對變量操作時的可見性泽腮,即一個線程修改了變量的值御蒲,對另一個線程是立馬可見的摔桦。
-
禁止指令重排序稍味,即在volatile語句之前的代碼不會重排序到volatile語句之后去執(zhí)行。
volatile
保證了可見性奶赔,是因?yàn)槭褂胿olatile關(guān)鍵字會強(qiáng)制把值寫入主內(nèi)存豪筝,同時將其他線程工作內(nèi)存中的緩存行無效痰滋,這樣其他線程在獲取變量值的時候,發(fā)現(xiàn)了自己的緩存行無效后续崖,在對應(yīng)的主內(nèi)存地址被更新后就會去主內(nèi)存中取敲街。volatile
保證了有序性,如果兩個操作的執(zhí)行次序無法從happens-befor
原則推導(dǎo)出來严望,那么它們就不能保證它們的有序性多艇,虛擬機(jī)可以隨意地對它們進(jìn)行重排序。volatile
底層是通過內(nèi)存屏障來完成一系列的有序性功能的像吻。注:
synchronized
和lock
也能保證有序性峻黍。
最后要強(qiáng)調(diào)一點(diǎn)的是,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);
}
}
這段代碼最終的輸出結(jié)果將小于10000姆涩,原因就在于inc++
它不是個原子性操作,盡管volatile
能保證對inc的修改立即被其他線程感知惭每,但是對于inc的并發(fā)讀取不會觸發(fā)強(qiáng)制刷新主內(nèi)存骨饿,也不會導(dǎo)致其他線程的緩存行無效,這樣就導(dǎo)致多個線程讀取到同樣的值台腥。
一般這種情況可以用synchronized
或lock
來解決宏赘,也可以通過無鎖CAS方式的AtomicInteger
來解決。
所以說黎侈,重量級鎖還是比較穩(wěn)的察署,volatile
不能完全替代synchronized
,使用volatile
必須具備兩個條件:
- 對變量的寫入不依賴當(dāng)前值
- 該變量沒有包含在其他變量的不變式中
參考資料
- 方騰飛:《Java并發(fā)編程的藝術(shù)》
- 指令重排序
- 深入分析volatile的實(shí)現(xiàn)原理