Java中的volatile關鍵字原理深入解析
volatile介紹
Java 語言規(guī)范 volatile 關鍵字定義:Java 編程語言允許線程訪問共享變量,為了確保共享變量能被準確和一致地更新,線程應該確保通過排他鎖單獨獲得這個變量。
volatile通常被比喻成"輕量級的synchronized",也是Java并發(fā)編程中比較重要的一個關鍵字。
和synchronized不同,volatile是一個變量修飾符,只能用來修飾變量。無法修飾方法及代碼塊等。
volatile的用法比較簡單,只需要在聲明一個可能被多線程同時訪問的變量時,使用volatile修飾就可以了。
private volatile static Singleton singleton;
volatile的原理
每個線程有自己的工作內存,線程的工作內存中保存了被該線程所使用到的變量(這些變量是從主內存中拷貝而來)。線程對變量的所有操作(讀取,賦值,抹除)都必須在工作內存中進行。不同線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成。
對于volatile變量,當對volatile變量進行寫操作的時候,JVM會向處理器發(fā)送一條lock前綴的指令,將這個緩存中的變量回寫到主存中。
但是就算寫回到主存,如果其他處理器緩存的值還是舊的,再執(zhí)行計算操作就會有問題,所以在多處理器下,為了保證各個處理器的緩存是一致的,就會實現(xiàn)緩存一致性協(xié)議。
緩存一致性協(xié)議:每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,當處理器發(fā)現(xiàn)自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態(tài),當處理器要對這個數(shù)據(jù)進行修改操作的時候,會強制重新從系統(tǒng)內存里把數(shù)據(jù)讀到處理器緩存里。
所以,如果一個變量被volatile所修飾的話,在每次數(shù)據(jù)變化之后,其值都會被強制刷入主存。而其他處理器的緩存由于遵守了緩存一致性協(xié)議,也會把這個變量的值從主存加載到自己的緩存中。這就保證了一個volatile在并發(fā)編程中,其值在多個緩存中是可見的。
volatile與可見性
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
Java內存模型規(guī)定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程的工作內存中保存了該線程中使用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量的傳遞均需要自己的工作內存和主存之間進行數(shù)據(jù)同步進行。所以,就可能出現(xiàn)線程1改了某個變量的值,但是線程2不可見的情況。
前面的關于volatile的原理中介紹過了,Java中的volatile關鍵字提供了一個功能,那就是被其修飾的變量在被修改后可以立即同步到主內存,被其修飾的變量在每次使用之前都從主內存刷新。因此,可以使用volatile來保證多線程操作時變量的可見性。
JVM對 volatile 的描述如下描述中寫著,如果字節(jié)碼中有用VOLATILE修飾的,代表這個變量不能被線程所緩存。
可以通過一下代碼進行驗證,你會發(fā)現(xiàn)該程序不會結束,但是如果你對flag加上了volatile修飾的話,結果就會不一樣了:
private static boolean flag = false;
public static void main(String[] args) {
new Thread(() -> {
flag = true;
}).start();
while (!aBoolean) {
}
System.out.println("end");
}
private static void run() volatile與有序性
有序性即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
普通的變量僅僅會保證在該方法的執(zhí)行過程中所依賴的賦值結果的地方都能獲得正確的結果,而不能保證變量的賦值操作的順序與程序代碼中的執(zhí)行順序一致。由于處理器優(yōu)化和指令重排等,CPU還可能對輸入代碼進行亂序執(zhí)行,比如load->add->save 有可能被優(yōu)化成load->save->add ,這就是有序性問題。
volatile可以禁止指令重排,這就保證了代碼的程序會嚴格按照代碼的先后順序執(zhí)行。這就保證了有序性。被volatile修飾的變量的操作,會嚴格按照代碼順序執(zhí)行,load->add->save 的執(zhí)行順序就是:load->add->save 。
重排序
為什么要有重排序呢?
簡單來說,就是為了提升執(zhí)行效率。
為什么能提升執(zhí)行效率呢?重排序可以提高程序的運行效率,但是必須遵循 as-if-serial 語義。as-if-serial 語義是什么呢?簡單來說,就是不管你怎么重排序,你必須保證不管怎么重排序,單線程下程序的執(zhí)行結果不能被改變(至于多線程就管不到了)。
內存屏障
如何保證CPU不會對這些操作進行重排序呢?
JVM是通過插入內存屏障保證的,JMM 規(guī)范中定義的內存屏障分為讀(load)屏障和寫(Store)屏障,排列組合就有了四種屏障。對于 volatile 操作,JMM 內存屏障插入策略:
- 在每個 volatile 寫操作的前面插入一個 StoreStore 屏障
- 在每個 volatile 寫操作的后面插入一個 StoreLoad 屏障
- 在每個 volatile 讀操作的后面插入一個 LoadLoad 屏障
- 在每個 volatile 讀操作的后面插入一個 LoadStore 屏障
而在 x86 處理器中,有三種方法可以實現(xiàn)實現(xiàn) StoreLoad 屏障的效果,分別為:
- mfence 指令:能實現(xiàn)全能型屏障,具備 lfence 和 sfence 的能力。
- cpuid 指令:cpuid 操作碼是一個面向 x86 架構的處理器補充指令,它的名稱派生自 CPU 識別,作用是允許軟件發(fā)現(xiàn)處理器的詳細信息。
- lock 指令前綴:總線鎖。lock 前綴只能加在一些特殊的指令前面。
lock指令
實際上 HotSpot 關于 volatile 的實現(xiàn)就是使用的 lock 指令,只在 volatile 標記的地方加上帶 lock 前綴指令操作,并沒有參照 JMM 規(guī)范的屏障設計。
lock 前綴指令實際上相當于一個內存屏障(也成內存柵欄),并提供以下三個功能:
- 它確保指令重排序時不會把其后面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的后面;即在執(zhí)行到內存屏障這句指令時,在它前面的操作已經(jīng)全部完成;
- 它會強制將對緩存的修改操作立即寫入主存;
- 如果是寫操作,它會導致其他 CPU 中對應的緩存行無效。
volatile與原子性
原子性是指一個操作是不可中斷的,要全部執(zhí)行完成,要不就都不執(zhí)行。
線程是CPU調度的基本單位。CPU有時間片的概念,會根據(jù)不同的調度算法進行線程調度。當一個線程獲得時間片之后開始執(zhí)行,在時間片耗盡之后,就會失去CPU使用權。所以在多線程場景下,由于時間片在線程間輪換,就會發(fā)生原子性問題。
為了保證原子性,需要通過字節(jié)碼指令monitorenter和monitorexit,但是volatile和這兩個指令之間是沒有任何關系的。所以,volatile是不能保證原子性的。
在以下兩個場景中可以使用volatile來代替synchronized:
- 運算結果并不依賴變量的當前值,或者能夠確保只有單一的線程會修改變量的值。
- 變量不需要與其他狀態(tài)變量共同參與不變約束。
除以上場景外,都需要使用其他方式來保證原子性,如synchronized或者JUC。
我們來看一下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);
}
}以上代碼比較簡單,就是創(chuàng)建10個線程,然后分別執(zhí)行1000次j++操作。正常情況下,程序的輸出結果應該是10000,但是,多次執(zhí)行的結果都小于10000。這其實就是volatile無法滿足原子性的原因。
對于一個簡單的i++操作,一共有三個步驟:load , add ,save 。共享變量就會被多個線程同時進行操作,這樣讀改寫操作就不是原子的,操作完之后共享變量的值可以會和期望的不一致。
DCL與volatile
為什么雙重校驗鎖實現(xiàn)的單例中,已經(jīng)使用了synchronized,為什么還需要volatile?
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
} 因為singleton = new Singleton()不是一個原子操作,大概要經(jīng)過這幾個步驟:
- 分配一塊內存空間
- 調用構造器,初始化實例
- singleton指向分配的內存空間
由于cpu重排序問題,導致實際執(zhí)行步驟可能是這樣的:
- 申請一塊內存空間
- singleton指向分配的內存空間
- 調用構造器,初始化實例
在singleton指向分配的內存空間之后,singleton就不為空了。
但是在沒有調用構造器初始化實例之前,這個對象還處于半初始化狀態(tài),在這個狀態(tài)下,實例的屬性都還是默認屬性,這個時候如果有另一個線程調用getSingleton()方法時,會拿到這個未初始化的對象,導致出錯。 而加 volatile 修飾之后,就會禁止重排序,這樣就能保證在對象初始化完了之后才把singleton指向分配的內存空間,杜絕了一些不可控錯誤的產(chǎn)生。volatile 提供了 happens-before 保證,Happens-before 主要是解決前一個操作的結果必須對后一個操作可見。
到此這篇關于Java中的volatile關鍵字原理深入解析的文章就介紹到這了,更多相關volatile關鍵字原理內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
springboot集成WebSockets廣播消息(推薦)
這篇文章主要介紹了springboot-集成WebSockets廣播消息,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-12-12
Spring和IDEA不推薦使用@Autowired?注解原因解析
這篇文章主要為大家介紹了Spring和IDEA不推薦使用@Autowired?注解原因解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07

