Volatile關鍵字的使用案例
Volatile關鍵字的作用主要有如下兩個:
1.線程的可見性:當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。
2. 順序一致性:禁止指令重排序。
一、線程可見性
我們先通過一個例子來看看線程的可見性:
public class VolatileTest { boolean flag = true; public void updateFlag() { this.flag = false; System.out.println("修改flag值為:" + this.flag); } public static void main(String[] args) { VolatileTest test = new VolatileTest(); new Thread(() -> { while (test.flag) { } System.out.println(Thread.currentThread().getName() + "結束"); }, "Thread1").start(); new Thread(() -> { try { Thread.sleep(2000); test.updateFlag(); } catch (InterruptedException e) { } }, "Thread2").start(); } }
打印結果如下,我們可以看到雖然線程Thread2已經把flag 修改為false了,但是線程Thread1沒有讀取到flag修改后的值,線程一直在運行
修改flag值為:false
我們把flag 變量加上volatile:
volatile boolean flag = true;
重新運行程序,打印結果如下。Thread1結束,說明Thread1讀取到了flage修改后的值
修改flag值為:false
Thread1結束
說到可見性,我們需要先了解一下Java內存模型,Java內存模型如下所示:
線程之間的共享變量存儲在主內存中(Main Memory)中,每個線程都一個都有一個私有的本地內存(Local Memory),本地內存中存儲了該線程以讀/寫共享變量的副本。
所以當一個線程把主內存中的共享變量讀取到自己的本地內存中,然后做了更新。在還沒有把共享變量刷新的主內存的時候,另外一個線程是看不到的。
如何把修改后的值刷新到主內存中的?
現(xiàn)代的處理器使用寫緩沖區(qū)臨時保存向內存寫入的數(shù)據(jù)。寫緩沖區(qū)可以保證指令流水線持續(xù)運行,它可以避免由于處理器停頓下來等向內存寫入數(shù)據(jù)而產生的延遲。同時,通過以批處理的方式刷新寫緩沖區(qū),以及合并寫緩沖區(qū)中對同一內存地址的多次寫,較少對內存總線的占用。但是什么時候寫入到內存是不知道的。
所以就引入了volatile,volatile是如何保證可見性的呢?
在X86處理器下通過工具獲取JIT編譯器生成的匯編指令來查看對volatile進行寫操作時,會多出lock addl。Lock前綴的指令在多核處理器下會引發(fā)兩件事情:
- 將當前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內存。
- 這個寫回內存的操作會使其他cpu里緩存了該內存地址的數(shù)據(jù)無效。
如果聲明了volatile的變量進行寫操作,JVM就會向處理器發(fā)送一條Lock前綴的指令,將這個變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內存。但是,就算寫回到內存,如果其他處理器緩存的還是舊的,在執(zhí)行操作就會有問題。所以,在多處理器下,為了保證各個處理器的緩存是一致的,就會實現(xiàn)緩存一致性協(xié)議,每個處理器通過嗅探在總線傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,當處理器發(fā)現(xiàn)自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態(tài),當處理器對這個數(shù)據(jù)進行修改操作的時候,會重新從系統(tǒng)內存中把數(shù)據(jù)讀到處理器緩存里。
二、順序一致性
在執(zhí)行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序。重排序分為如下三種:
1屬于編譯器重排序,2和3屬于處理器重排序。這些重排序可能會導致多線程程序出現(xiàn)內存可見性問題。
當變量聲明為volatile時,Java編譯器在生成指令序列時,會插入內存屏障指令。通過內存屏障指令來禁止重排序。
JMM內存屏障插入策略如下:
在每個volatile寫操作的前面插入一個StoreStore屏障,后面插入一個StoreLoad屏障。
在每個volatile讀操作后面插入一個LoadLoad,LoadStore屏障。
Volatile寫插入內存屏障后生成指令序列示意圖:
Volatile讀插入內存屏障后生成指令序列示意圖:
通過上面這些我們可以得出如下結論:編譯器不會對volatile讀與volatile讀后面的任意內存操作重排序;編譯器不會對volatile寫與volatile寫前面的任意內存操作重排序。
防止重排序使用案例:
public class SafeDoubleCheckedLocking { private volatile static Instance instane; public static Instance getInstane(){ if(instane==null){ synchronized (SafeDoubleCheckedLocking.class){ if(instane==null){ instane=new Instance(); } } } return instane; } }
創(chuàng)建一個對象主要分為如下三步:
- 分配對象的內存空間。
- 初始化對象。
- 設置instance指向內存空間。
如果instane 不加volatile,上面的2,3可能會發(fā)生重排序。假設A,B兩個線程同時獲取,A線程獲取到了鎖,發(fā)生了指令重排序,先設置了instance指向內存空間。這個時候B線程也來獲取,instance不為空,這樣B拿到了沒有初始化完成的單例對象(如下圖)
二、Volatile與Synchronized比較
- Volatile是輕量級的synchronized,因為它不會引起上下文的切換和調度,所以Volatile性能更好。
- Volatile只能修飾變量,synchronized可以修飾方法,靜態(tài)方法,代碼塊。
- Volatile對任意單個變量的讀/寫具有原子性,但是類似于i++這種復合操作不具有原子性。而鎖的互斥執(zhí)行的特性可以確保對整個臨界區(qū)代碼執(zhí)行具有原子性。
- 多線程訪問volatile不會發(fā)生阻塞,而synchronized會發(fā)生阻塞。
- volatile是變量在多線程之間的可見性,synchronize是多線程之間訪問資源的同步性。
到此這篇關于Volatile關鍵字的作用的文章就介紹到這了,更多相關Volatile關鍵字的作用內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java排序算法三之歸并排序的遞歸與非遞歸的實現(xiàn)示例解析
這篇文章主要介紹了Java排序算法三之歸并排序的遞歸與非遞歸的實現(xiàn)示例解析,文章通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-08-08IDEA 2020.3.X 創(chuàng)建scala環(huán)境的詳細教程
這篇文章主要介紹了IDEA 2020.3.X 創(chuàng)建scala環(huán)境的詳細教程,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-04-04java使用MulticastSocket實現(xiàn)基于廣播的多人聊天室
這篇文章主要為大家詳細介紹了java使用MulticastSocket實現(xiàn)基于廣播的多人聊天室,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-01-01使用Spring Boot創(chuàng)建Web應用程序的示例代碼
本篇文章主要介紹了使用Spring Boot創(chuàng)建Web應用程序的示例代碼,我們將使用Spring Boot構建一個簡單的Web應用程序,并為其添加一些有用的服務,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05springboot中spring.profiles.include的妙用分享
這篇文章主要介紹了springboot中spring.profiles.include的妙用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08解決多模塊項目中Mybatis的Mapper內部方法找不到的問題
這篇文章主要介紹了解決多模塊項目中Mybatis的Mapper內部方法找不到的問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11