這是 wanAndroid 每日一問中的一道題,下面我們來嘗試解答一下。
講講並發專題 volatile,synchronize,CAS,happens before, lost wake up
為了本系列的「短平快」,今天我們就來第一個主角:volatile。
保證內存可見性
前面我們講到:Java 內存模型分為了主內存和工作內存兩部分,其規定程序所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程的工作內存中保存了該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(賦值、讀取等)都必須在工作內存中進行,而不能直接讀取主內存中的變量。不同線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞都必須經過主內存的傳遞來完成。

這樣就會存在一個情況,工作內存值改變后到主內存更新一定是需要一定時間的,所以可能會出現多個線程操作同一個變量的時候出現取到的值還是未更新前的值。
這樣的情況我們通常稱之為「可見性」,而我們加上 volatile 關鍵字修飾的變量就可以保證對所有線程的可見性。
這里的可見性是什么意思呢?當一個線程修改了變量的值,新的值會立刻同步到主內存當中。而其他線程讀取這個變量的時候,也會從主內存中拉取最新的變量值。
為什么 volatile 關鍵字可以有這樣的特性?這得益於 Java 語言的先行發生原則(happens-before)。簡單地說,就是先執行的事件就應該先得到結果。
但是! volatile 並不能保證並發下的安全。
Java 里面的運算並非原子操作,比如 i++ 這樣的代碼,實際上,它包含了 3 個獨立的操作:讀取 i 的值,將值加 1,然后將計算結果返回給 i。這是一個「讀取-修改-寫入」的操作序列,並且其結果狀態依賴於之前的狀態,所以在多線程環境下存在問題。
要解決自增操作在多線程下線程不安全的問題,可以選擇使用 Java 提供的原子類,如
AtomicInteger或者使用synchronized同步方法。原子性:在 Java 中,對基本數據類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要么執行,要么不執行。也就是說,只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量)才是原子操作。(變量之間的相互賦值不是原子操作,比如
y = x,實際上是先讀取x的值,再把讀取到的值賦值給y寫入工作內存)
禁止指令重排
最開始看到「指令重排」這個詞語的時候,我也是一臉懵逼。后面看了相關書籍才知道,處理器為了提高程序效率,可能對輸入代碼進行優化,它不保證各個語句的執行順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。
指令重排是一把雙刃劍,雖然優化了程序的執行效率,但是在某些情況下,卻會影響到多線程的執行結果。比如下面的代碼:
boolean contextReady = false;
//在線程A中執行:
context = loadContext(); // 步驟 1
contextReady = true; // 步驟 2
//在線程B中執行:
while(!contextReady ){
sleep(200);
}
doAfterContextReady (context);
以上程序看似沒有問題。線程 B 循環等待上下文 context 的加載,一旦 context 加載完成,contextReady == true 的時候,才執行 doAfterContextReady 方法。
但是,如果線程 A 執行的代碼發生了指令重排,也就是上面的步驟 1 和步驟 2 調換了順序,那線程 B 就會直接跳出循環,直接執行 doAfterContextReady() 方法導致出錯。
而 volatile 采用「內存屏障」這樣的 CPU 指令就解決這個問題,不讓它指令重排。
使用場景
從上面的總結來看,我們非常容易得出 volatile 的使用場景:
- 運行結果並不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。
- 變量不需要與其他的狀態變量共同參與不變約束。
比如下面的場景,就很適合使用 volatile 來控制並發,當 shutdown() 方法調用的時候,就能保證所有線程中執行的 work() 立即停下來。
volatile boolean shutdownRequest;
private void shutdown(){
shutdownRequest = true;
}
private void work(){
while (!shutdownRequest){
// do something
}
}
總結
說了這么多,其實對於 volatile 我們只需要知道,它主要特性:保證可見性、禁止指令重排、解決 long 和 double 的 8 字節賦值問題。
還有一個比較重要的是:它並不能保證並發安全,不要和 synchronize 混淆。
細心的你還會發現,在 Kotlin 語言中,其實是沒有
volatile和synchronize這樣的關鍵字的,那 Kotlin 是怎么處理並發問題的呢?感興趣的一定要去看看。
文章參考:
漫畫:什么是volatile關鍵字?(整合版)
《深入理解 Java 虛擬機》
