天天在用volatile,你知道它的底層原理嗎?

前言

對於從事java開發工作的朋友來說,在工作中可能會經常接觸volatile關鍵字。即使有些朋友沒有直接使用volatile關鍵字,但是如果使用過:ConcurrentHashMap、AtomicInteger、FutureTask、ThreadPoolExecutor等功能,它們的底層都使用了volatile關鍵字,你就不想了解一下它們為什麼要使用volatile關鍵字,它的底層原理是什麼?

從雙重檢查鎖開始

面試時被要求寫個單例模式的程式碼,很多朋友可能寫的是雙重檢查鎖。程式碼如下:

public class SimpleSingleton4 { private static SimpleSingleton4 INSTANCE; private SimpleSingleton4() { } public static SimpleSingleton4 getInstance() { if (INSTANCE == null) { synchronized (SimpleSingleton4。class) { if (INSTANCE == null) { INSTANCE = new SimpleSingleton4(); } } } return INSTANCE; }}

有些朋友看到這裡覺得有點熟悉,平時可能就是這個寫的。

但是,我要告訴你的是,這個程式碼有問題,它在有些時候不是單例的。為什麼會出現問題呢?

答案,在後面揭曉。

JMM(java記憶體模型)

在介紹volatile底層原理之前,讓我們先看看什麼是JMM(即java記憶體模型)。

天天在用volatile,你知道它的底層原理嗎?

java記憶體模型中規定所有變數都儲存在主記憶體,主記憶體是共享記憶體區域,所有執行緒都可以訪問,但執行緒對變數的操作(讀取賦值等)必須在工作記憶體中進行,首先要將變數從主記憶體複製的自己的工作記憶體空間,然後對變數進行操作,操作完成後再將變數寫回主記憶體,不能直接操作主記憶體中的變數,工作記憶體中儲存著主記憶體中的變數副本複製。前面說過,工作記憶體是每個執行緒的私有資料區域,因此不同的執行緒間無法訪問對方的工作記憶體,執行緒間的通訊(傳值)必須透過主記憶體來完成。

java記憶體模型會帶來三個問題:

1.可見性問題

執行緒A和執行緒B同時操作共享資料C,執行緒A修改的結果,執行緒B是不知道的,即不可見的

2.競爭問題

剛開始資料C的值為1,執行緒A和執行緒B同時執行加1操作,正常情況下資料C應該為3,但是在併發的情況下,資料C卻還是2

3.重排序問題

JVM為了最佳化指令的執行效率,會對一下程式碼指令進行重排序。

那麼如何解決問題呢?

volatile的底層原理

java 編譯器在生成指令序列的適當位置會插入記憶體屏障指令來禁止特定類 型的處理器重排序,從而讓程式按我們預想的流程去執行。

1、保證特定操作的執行順序。

2、影響某些資料(或則是某條指令的執行結果)的記憶體可見性。

java的記憶體屏障指令如下:

天天在用volatile,你知道它的底層原理嗎?

對於volatile的寫操作,在其前後分別加上 StoreStore 和 StoreLoad指令

天天在用volatile,你知道它的底層原理嗎?

對於volatile的讀操作,在其後加上 LoadLoad 和 LoadStore指令

天天在用volatile,你知道它的底層原理嗎?

由上圖可以看到,記憶體屏障是可以保證volatile變數前後讀寫順序的。

此外,對volatile變數寫操作時,使用store指令會強制執行緒重新整理資料到主記憶體,讀操作使用load指令會強制從主記憶體讀取變數值。

再看看這個例子:

public class DataTest { private volatile int count = 0; public int getCount() { return count; } public void setCount(int count) { this。count = count; } public void incr() { count++; } }

上面列子中的getCount和setCount方法這種單操作是可以保證原子性的,但是像incr方法無法保證原子性。

由此可見,volatile關鍵字可以解決可見性 和 重排序問題。但是不能解決競爭問題,無法保證操作的原子性,解決競爭問題需要加鎖,或者使用cas等無鎖技術。

再看雙重檢查鎖問題

從上面可以看出JMM會有重排序問題,之前雙重檢查鎖為什麼有問題呢?

public static SimpleSingleton4 getInstance() { if (INSTANCE == null) { synchronized (SimpleSingleton4。class) { if (INSTANCE == null) { //1。分配記憶體空間 //2。初始化引用 //3。將實際的記憶體地址賦值給當前引用 INSTANCE = new SimpleSingleton4(); } } } return INSTANCE;}

從程式碼中的註釋可以看出,INSTANCE = new SimpleSingleton4();這一行程式碼其實經歷了三個過程:

1。分配記憶體空間

2。初始化引用

3。將實際的記憶體地址賦值給當前引用

正常情況下是按照1、2、3的順序執行的,但是指令重排之後也不排除按照1、3、2的順序執行的可能性,如果按照1、3、2的順序。

天天在用volatile,你知道它的底層原理嗎?

上面錯誤雙重檢查鎖定的示例程式碼中,如果執行緒 1 獲取到鎖進入建立物件例項,這個時候發生了指令重排序。當執行緒1 執行到 t3 時刻,執行緒 2 剛好進入,由於此時物件已經不為 Null,所以執行緒 2 可以自由訪問該物件。然後該物件還未初始化,所以執行緒 2 訪問時將會發生異常。

解決這個問題,可以把INSTANCE定義成volatile的。

private volatile static SimpleSingleton4 INSTANCE;

其實,建立單例的方法有很多,最好的還是靜態內部類。

public class SimpleSingleton5 { private SimpleSingleton5() { } public static SimpleSingleton5 getInstance() { return Inner。INSTANCE; } private static class Inner { private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5(); }}

總結

volatile的底層是透過:store,load等記憶體屏障命令,解決JMM的可見性和重排序問題的。但是它無法解決競爭問題,要解決競爭問題需要加鎖,或使用cas等無鎖技術。單例模式不建議使用雙重檢查鎖,推薦使用靜態內部類的方式建立。

彩蛋

使用volatile保證執行緒間的可見性和重排序問題,相對於synchronized等加鎖機制更輕量級,但是對效能還是有一定的消耗,如何最佳化效能呢?

可以參考spring中DefaultNamespaceHandlerResolver類的getHandlerMappings方法

@Nullableprivate volatile Map handlerMappings;……。private Map getHandlerMappings() { Map handlerMappings = this。handlerMappings; if (handlerMappings == null) { synchronized (this) { handlerMappings = this。handlerMappings; if (handlerMappings == null) { if (logger。isDebugEnabled()) { logger。debug(“Loading NamespaceHandler mappings from [” + this。handlerMappingsLocation + “]”); } try { Properties mappings = PropertiesLoaderUtils。loadAllProperties(this。handlerMappingsLocation, this。classLoader); if (logger。isDebugEnabled()) { logger。debug(“Loaded NamespaceHandler mappings: ” + mappings); } handlerMappings = new ConcurrentHashMap<>(mappings。size()); CollectionUtils。mergePropertiesIntoMap(mappings, handlerMappings); this。handlerMappings = handlerMappings; } catch (IOException ex) { throw new IllegalStateException( “Unable to load NamespaceHandler mappings from location [” + this。handlerMappingsLocation + “]”, ex); } } } } return handlerMappings;}

該方法就使用了雙重檢查鎖,可以看到方法內部使用區域性變數,首先將例項變數值賦值給該區域性變數,然後再進行判斷。最後內容先寫入區域性變數,然後再將區域性變數賦值給例項變數。使用區域性變數相對於不使用區域性變數,可以提高效能。主要是由於

volatile

變數建立物件時需要禁止指令重排序,這就需要一些額外的操作。

如果這篇文章對您有所幫助,或者有所啟發的話,幫忙關注一下:蘇三說技術,或者點贊,轉發一下,堅持原創不易,您的支援是我前進最大的動力,謝謝。