java并發(fā)編程關鍵字volatile保證可見性不保證原子性詳解
volatile關鍵字可以說是Java虛擬機提供的最輕量級的同步機制,但對于為什么它只能保證可見性,不保證原子性,它又是如何禁用指令重排的,還有很多同學沒徹底理解
相信我,堅持看完這篇文章,你將牢牢掌握一個Java核心知識點
先說它的兩個作用:
保證變量在內存中對線程的可見性禁用指令重排
每個字都認識,湊在一起就麻了
這兩個作用通常很不容易被我們Java開發(fā)人員正確、完整地理解,以至于許多同學不能正確地使用volatile
關于可見性
不多bb,碼來
public class VolatileTest { private static volatile int count = 0; private static void increase() { count++; } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { new Thread(() -> { for (int j = 0; j < 10000; j++) { increase(); } }).start(); } // 所有線程累加完成后輸出 while (Thread.activeCount() > 2) Thread.yield(); System.out.println(count); } }
代碼很好理解,開了十個線程對同一個共享變量count做累加,每個線程累加1w次
count我們已經(jīng)用volatile修飾,已經(jīng)保證了count對十個線程在內存中的可見性,按理說十個線程執(zhí)行完畢count的值應該10w
然鵝,運行多次,結果都遠小于期望值
是哪個環(huán)節(jié)出了問題?
你肯定聽過一句話:volatile只保證可見性,不保證原子性
這句話就是答案,但是依舊很多人沒搞懂其中的奧秘
說來話長我長話短說,簡單來講就是 count++這個操作不是原子的,它是分三步進行
- 從內存讀取 count 的值
- 執(zhí)行 count + 1
- 將 count 的新值寫回
要徹底搞懂這個問題,我們得從字節(jié)碼入手
下面是increase方法編譯后的字節(jié)碼
看不懂沒關系,我們一行一行來看:
- GETSTATIC:讀取 count 的當前值
- ICONST_1:將常量 1 加載到棧頂
- IADD:執(zhí)行+1
- PUTSTATIC:寫入count最新值
ICONST_1和IADD其實就是真正的++操作
關鍵點來了,volatile只能保證線程在GETSTATIC這一步拿到的值是最新的,但當該線程執(zhí)行到下面幾行指令時,這期間可能就有其它線程把count的值修改了,最終導致舊值把真正的新值覆蓋
懂我意思嗎
所以,并發(fā)編程中,只靠volatile修飾共享變量是不可靠的,最終還是要通過對關鍵方法加鎖來保證線程安全
就如上面的demo,稍加修改就能實現(xiàn)真正的線程安全
最簡單的,給increase方法加個synchronized (synchronized怎么實現(xiàn)線程安全的我就不啰嗦了,我以前講過 synchronized底層實現(xiàn)原理)
private synchronized static void increase() { ++count; }
run幾下
這不就妥了嘛
到現(xiàn)在,對于以下兩點你應該有了新的認知
volatile保證變量在內存中對線程的可見性
volatile只保證可見性,不保證原子性
關于指令重排
并發(fā)編程中,cpu自身和虛擬機為了提高執(zhí)行效率,都會采用指令重排(在保證不影響結果的前提下,將某些代碼亂序執(zhí)行)
- 關于cpu:為了從分利用cpu,實際執(zhí)行指令時會做優(yōu)化;
- 關于虛擬機:在HotSpot vm中,為了提升執(zhí)行效率,JIT(即時編譯)模式也會做指令優(yōu)化
指令重排在大部分場景下確實能提升執(zhí)行效率,但有些場景對代碼執(zhí)行順序是強依賴的,此時我們需要禁用指令重排,如下面這個場景
偽代碼取自《深入理解Java虛擬機》:
其描述的場景是開發(fā)中常見配置讀取過程,只是我們在處理配置文件時一般不會出現(xiàn)并發(fā),所以沒有察覺這會有問題。
試想一下,如果定義initialized變量時沒有使用volatile修飾,就可能會由于指令重排序的優(yōu)化,導致位于線程A中最后一條代碼“initialized=true”被提前執(zhí)行(這里雖然使用Java作為偽代碼,但所指的重排序優(yōu)化是機器級的優(yōu)化操作,提前執(zhí)行是指這條語句對應的匯編代碼被提前執(zhí)行),這樣在線程B中使用配置信息的代碼就可能出現(xiàn)錯誤,而volatile通過禁止指令重排則可以避免此類情況發(fā)生
禁用指令重排只需要將變量聲明為volatile,是不是很神奇
我們來看看volatile是如何實現(xiàn)禁用指令重排的
也借用《深入理解Java虛擬機》的一個例子吧,比較好理解
這是個單例模式的實現(xiàn),下面是它的部分字節(jié)碼,紅框中 mov%eax,0x150(%esi) 是對instance賦值
可以看到,在賦值后,還執(zhí)行了 lock addl$0x0,(%esp) 指令,關鍵點就在這兒,這行指令相當于此處設置了個 內存屏障 ,有了內存屏障后,cpu或虛擬機在指令重排時就不能把內存屏障后面的指令提前到內存屏障前面,好好捋一下這段話
最后,留一個能加深大家對volatile理解的問題,兄弟們好好思考下:
Java代碼明明是從上往下依次執(zhí)行,為什么會出現(xiàn)指令重排這個問題?
以上就是java并發(fā)編程關鍵字volatile保證可見性不保證原子性詳解的詳細內容,更多關于java并發(fā)編程關鍵字volatile的資料請關注腳本之家其它相關文章!
相關文章
Java實現(xiàn)FIFO、LRU、LFU、OPT頁面置換算法
本文主要介紹了Java實現(xiàn)FIFO、LRU、LFU、OPT頁面置換算法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-02-02Mybatis使用MySQL模糊查詢時輸入中文檢索不到結果怎么辦
這篇文章主要介紹了Mybatis使用MySQL模糊查詢時輸入中文檢索不到結果的解決辦法的相關資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-07-07詳解java WebSocket的實現(xiàn)以及Spring WebSocket
這篇文章主要介紹了詳解java WebSocket的實現(xiàn)以及Spring WebSocket ,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-01-01Mybatis-Plus的saveOrUpdateBatch(null)問題及解決
這篇文章主要介紹了Mybatis-Plus的saveOrUpdateBatch(null)問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07