在多線程編程中粘昨,Synchronized 和 volatile 都扮演者重要的角色,前面的文章我們已經了解了java內置鎖Synchronized 盗迟,它保證了并發(fā)過程中的可見性與原子性,避免了共享數(shù)據(jù)的錯誤。
而 Volatile可以看做是輕量級的 Synchronized厌漂,它只保證了共享變量的可見性。在線程 A 修改了被 volatile 修飾的共享變量之后斟珊,線程 B 能夠讀取到正確的值苇倡。在 關于JMM 的文章中我們了解到 java 在多線程中操作共享變量的過程中,會存在指令重排序與共享變量工作內存緩存的問題囤踩。
volatile作為一個修飾符旨椒,使用很簡單,但是它背后做了多少工作呢堵漱?
首先我們需要明白综慎,本地內存是一個抽象概念,包括緩存勤庐、讀寫緩沖區(qū)示惊、寄存器好港,甚至編譯器重排序和cpu重排序。JVM按照JMM規(guī)范對volatile進行特殊處理米罚,從而實現(xiàn)在CPU對該變量的特殊處理钧汹。
volatile底層原理
計算機系統(tǒng)中,硬盤負責存儲數(shù)據(jù)录择, 但是數(shù)據(jù)交換速度慢拔莱,CPU 運行速度非常快隘竭,CPU直接硬盤的數(shù)據(jù)交換效率非常低塘秦,于是產生了內存,通過內存與 CPU 進行數(shù)據(jù)交換动看,但是內存的速度依舊不夠快嗤形,嚴重拖慢整體的運行效率,故而在 CPU 內部添加了高速緩存弧圆,作為 CPU的臨時存儲器赋兵,與內存的數(shù)據(jù)交互。
- 在單核CPU中搔预,多線程都在一個CPU中進行運行霹期,共用一份緩存,對同一個共享變量的使用拯田,而不會出現(xiàn)數(shù)據(jù)可見性的問題
- 而多核CPU由于多線程可能分配在不同的CPU历造,這種情況下進行計算時,就會出現(xiàn)一個CPU內核計算完成船庇,并沒有同步回主內存吭产,而其他CPU無法使用最新的數(shù)據(jù),而出現(xiàn)了可見性問題鸭轮。
通過添加volatile修飾臣淤,通過JVM的優(yōu)化,最后反應到CPU上窃爷,先從內存獲取數(shù)據(jù)邑蒋,存儲在高速緩存中,然后再從高速緩存中獲取數(shù)據(jù)進行計算按厘,計算完成后的值并不會立即刷新回主內存中医吊,而其他 CPU 這時并不知道變量值已經改變,使用的還是之前的變量值進行計算逮京,這就產生了數(shù)據(jù)錯誤卿堂。這種機制類似我們之前講過的 JMM 中主內存于工作內存的關系。
我們知道懒棉,javac 編譯器將 .java 代碼編譯成為 .class 字節(jié)碼草描,JVM 通過解釋器與即時編譯器(JIT)運行字節(jié)碼中的指令览绿,將字節(jié)碼指令翻譯稱為具體的機器碼指令,而被 volatile 修飾的共享變量陶珠,在翻譯成為機器碼的過程中為其賦值操作
添加特殊機器碼指令前綴Lock xxxx
。
public class Test{
private volatile int i=1;//被 volatile 修飾
//線程A修改
public void setVar(){
i=2;
}
//線程B獲取
public int getVar(){
return i享钞;
}
}
在執(zhí)行此條指令時揍诽,Lock 指令有兩個作用:
- 使本CPU的緩存寫入內存
- 上面的寫入動作也會引起別的CPU或別的內核中的緩存無效,
所以通過這樣一個指令前綴栗竖,可以讓對volatile變量的修改對其他CPU可見暑脆。
指令重排序
還是上文中的Lock前綴的作用,為什么它能禁止指令重排序呢狐肢?
從JMM角度講:
在JMM的邏輯實現(xiàn)中添吗,當操作一個變量 執(zhí)行putfield
指令(為變量賦值) 時,JVM會檢查此變量是否是被volatile修飾的份名,如果是的話碟联,JVM會為該變量添加內存屏障,用于隔離該變量與前后操作僵腺,從而禁止volatile變量的操作與前后操作的亂序鲤孵。
摘自java并發(fā)編程的藝術:
為了實現(xiàn)volatile的內存語義,編譯器在生成字節(jié)碼時辰如,會在指令序列中插入內存屏障來
禁止特定類型的處理器重排序普监。對于編譯器來說,發(fā)現(xiàn)一個最優(yōu)布置來最小化插入屏障的總
數(shù)幾乎不可能琉兜。為此凯正,JMM采取保守策略。下面是基于保守策略的JMM內存屏障插入策略豌蟋。
·在每個volatile寫操作的前面插入一個StoreStore屏障廊散。
·在每個volatile寫操作的后面插入一個StoreLoad屏障。
·在每個volatile讀操作的后面插入一個LoadLoad屏障梧疲。
·在每個volatile讀操作的后面插入一個LoadStore屏障奸汇。
從CPU執(zhí)行角度講:
以上的內存屏障就會在執(zhí)行時生成相應帶有Lock
前綴的機器碼(全面已提及)。在CPU中往声,程序的執(zhí)行計算是由CPU在不影響邏輯結果的前提下分配給不同的電路去處理邏輯擂找,Lock指令前綴刷新回內存,必然是在此指令之前的運算全部計算完成之后浩销,取得正確的結果才會刷新回內存的贯涎,所以這也形成了一道內存屏障,表示對該變量操作之前的操作不會亂序到其后慢洋,其后的操作不會亂序到之前塘雳。
綜上述陆盘,volatile的實現(xiàn)就是一個Lock
指令前綴的作用。
使用注意事項
volatile雖然保證了可見性败明,但是它不保證原子性隘马。
諸如i++
之類的語句,在執(zhí)行時的步驟:
- 從內存取值妻顶,放到CPU緩存中
- CPU中i+1
- 存在緩存中
- 刷新會內存
可見這這并不是單純的賦值操作酸员,而是有在第4步完成之前,其他CPU內核是看不到數(shù)值變化的讳嘱,而如果僅用volatile修飾的話幔嗦,僅僅保證了第3部完成之后,會立即刷新回內存沥潭,但不會保證第2步計算與第3邀泉,4步的原子性。如果線程A計算+1之后钝鸽,沒有刷回內存汇恤,線程B也+1,那么最后的結果肯定是比期望的結果小的拔恰。所以在多線程操作++
時屁置,還是應該使用synchronized
等同步操作保證原子性。
volatile比synchronized輕量仁连,只保證可見性蓝角。正因如此,在java.util.concurrent
中AQS使用了被volatile修飾的變量來標記狀態(tài)饭冬,實現(xiàn)了靈活多樣的各種鎖使鹅,補充了內置鎖synchronized的互斥等缺點。