每日一問:談談 volatile 關鍵字

這是 wanAndroid 每日一問中的一道題,下面咱們來嘗試解答一下。java

講講併發專題 volatile,synchronize,CAS,happens before, lost wake upandroid

爲了本系列的「短平快」,今天咱們就來第一個主角:volatile安全

保證內存可見性

前面咱們講到:Java 內存模型分爲了主內存和工做內存兩部分,其規定程序全部的變量都存儲在主內存中,每條線程還有本身的工做內存,線程的工做內存中保存了該線程使用到的變量的主內存副本拷貝,線程對變量的全部操做(賦值、讀取等)都必須在工做內存中進行,而不能直接讀取主內存中的變量。不一樣線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞都必須通過主內存的傳遞來完成。
多線程

這樣就會存在一個狀況,工做內存值改變後到主內存更新必定是須要必定時間的,因此可能會出現多個線程操做同一個變量的時候出現取到的值仍是未更新前的值。併發

這樣的狀況咱們一般稱之爲「可見性」,而咱們加上 volatile 關鍵字修飾的變量就能夠保證對全部線程的可見性。app

這裏的可見性是什麼意思呢?當一個線程修改了變量的值,新的值會馬上同步到主內存當中。而其餘線程讀取這個變量的時候,也會從主內存中拉取最新的變量值。優化

爲何 volatile 關鍵字能夠有這樣的特性?這得益於 Java 語言的先行發生原則(happens-before)。簡單地說,就是先執行的事件就應該先獲得結果。線程

可是! volatile 並不能保證併發下的安全。code

Java 裏面的運算並不是原子操做,好比 i++ 這樣的代碼,實際上,它包含了 3 個獨立的操做:讀取 i 的值,將值加 1,而後將計算結果返回給 i。這是一個「讀取-修改-寫入」的操做序列,而且其結果狀態依賴於以前的狀態,因此在多線程環境下存在問題。blog

要解決自增操做在多線程下線程不安全的問題,能夠選擇使用 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 的使用場景:

  1. 運行結果並不依賴變量的當前值,或者可以確保只有單一的線程修改變量的值。
  2. 變量不須要與其餘的狀態變量共同參與不變約束。

好比下面的場景,就很適合使用 volatile 來控制併發,當 shutdown() 方法調用的時候,就能保證全部線程中執行的 work() 當即停下來。

volatile boolean shutdownRequest;
private void shutdown(){
    shutdownRequest = true;
}
private void work(){
    while (!shutdownRequest){
        // do something
    }
}

總結

說了這麼多,其實對於 volatile 咱們只須要知道,它主要特性:保證可見性、禁止指令重排、解決 long 和 double 的 8 字節賦值問題。

還有一個比較重要的是:它並不能保證併發安全,不要和 synchronize 混淆。

細心的你還會發現,在 Kotlin 語言中,實際上是沒有 volatilesynchronize 這樣的關鍵字的,那 Kotlin 是怎麼處理併發問題的呢?感興趣的必定要去看看。

文章參考:
漫畫:什麼是volatile關鍵字?(整合版) 《深刻理解 Java 虛擬機》

相關文章
相關標籤/搜索