volatile是什么?volatile能保證線程安全性嗎?如何正確使用volatile?


1. volatile是什么?
  在談及線程安全時,常會說到一個變量——volatile。在《Java並發編程實戰》一書中是這么定義volatile的——“Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操作通知到其他線程”。這句話說明了兩點:①volatile變量是一種同步機制;②volatile能夠確保可見性。這兩點和我們探討“volatile變量是否能夠保證線程安全性”息息相關。

 

2. volatile變量能確保線程安全性嗎?為什么?

  什么是同步機制?在並發程序設計中,各進程對公共變量的訪問必須加以制約,這種制約稱為同步。也就是說,同步機制即為對共享資源的一種制約。那么問題來了:volatile這種“稍弱的同步機制”是怎么制約各個進程對共享資源的訪問的呢?答案就在“volatile能夠確保可見性”中。

2.1 可見性

  volatile能夠保證字段的可見性:volatile變量,用來確保將變量的更新操作通知到其他線程。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。

  可見性和“線程如何對變量進行操作(取值、賦值等)”有關系:

  我們要先明確一個定律線程對變量的所有操作(取值、賦值等)都必須在工作內存(各線程獨立擁有)中進行,而不能直接讀寫內存中的變量,各工作內存間也不能相互訪問。對於volatile變量來說,由於它特殊的操作順序性規定,看起來如同操作主內存一般,但實際上 volatile變量也是遵循這一定律的。

  關於主存與工作內存之間具體的交互協議(即一個變量如何從主存拷貝到工作內存、如何從工作內存同步到主存等實現細節),Java內存模型中定義了以下八種操作來完成:

    lock:(鎖定),unlock(解鎖),read(讀取),load(載入),use(試用), assign(賦值),store(存儲),write(寫入)。

  volatile 對這八種操作有着兩個特殊的限定,正因為有這些限定才讓volatile修飾的變量有可見性以及可以禁止指令重排序 :

    ① use動作之前必須要有read和load動作, 這三個動作必須是連續出現的。【表示:每次工作內存要使用volatile變量之前必須去主存中拿取最新的volatile變量】

    ② assign動作之后必須跟着store和write動作,這三個動作必須是連續出現的。【表示: 每次工作內存改變了volatile變量的值,就必須把該值寫回到主存中】

  有以上兩條規則就能保證每個線程每次去拿volatile變量的時候,那個變量肯定是最新的, 其實也就相當於好多個線程用的是同一個內存,無工作內存和主存之分。而操作沒有用volatile修飾的變量則不能保證每次都能獲取到最新的變量值。

2.2 所以volatile究竟能否保證線程安全性?

  答:在某些特定的情況下能。那到底什么是什么能,什么時候又不能了呢?我們繼續往下看。

  (1)volatile能保證線程安全的情況

  要使 volatile 變量提供理想的線程安全性,必須同時滿足兩個條件:①對變量的寫操作不依賴於當前值。②該變量沒有包含在具有其他變量的不變式中。

  實際上,這兩個條件表明,可以被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。大多數編程情形都會與這兩個條件的其中之一沖突(如:“若沒有則添加”、“若相等則移出”的復合操作等復合操作都是與①或②相沖突的),使得 volatile 變量不能像 synchronized 那樣普遍適用於實現線程安全。

 

  (2)volatile不能保證線程安全的情況

  除了(1)中提到的能夠使volatile發揮保證線程安全性的情況,其他情況下volatile並不能保證線程安全問題,因為volatile並不能保證變量操作的原子性。

  我們先以 i++( i++ 是非原子操作)為例:

    private volatile int i = 0,兩個線程同時執行 i++,

    此時是兩個線程同時從主內存中拿到 i 的最新值 0 ,並且同時對 i 進行 +1 操作並將和賦值回 i,最后同時將 +1 后的 i 值寫回主內存中,最終 i == 1,很明顯結果是錯的。

  下面我們再通過更詳細的代碼來驗證“即使變量用了volatile來修飾,才進行非原子操作時依舊會出現線程安全問題”:

class Window implements Runnable {
    private volatile int ticket = 100;

    public void run() {
        for (;;) {
            //通過下面的①②兩個步驟我們可以發現:對一個共享資源可以多個線程同時進行修改,自然就會有線程安全問題。
            if (ticket > 0) {
                try {
                    Thread.sleep(100);//①多個線程同時判斷到“ticket>0”,然后掛起了
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //②多個線程同時醒來,同時進行“ticket--”操作:
                System.out.println(Thread.currentThread().getName() + ":" + ticket--);
            } else {
                break;
            }
        }
    }
}
public class A03UseVolatileIsNotThreadSafe {
    public static void main(String[] args) {
        Window w = new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

  測試結果:(1)出現了大量的重復數字; (2)最后還輸出了 “-1”;==》說明變量即使用volatile修飾了但依舊出現了線程安全問題。

  代碼解析:
    出現問題(1)的原因:線程存在“先檢查后執行”的競態條件。可能有兩個線程同時擁有CPU的執行權(機器是雙核的),它們判斷到做“if (ticket > 0)”,並同時做“ticket--”操作。
    出現問題(2)的原因:
      ①當ticket==1時,兩個或多個線程同時通過了“if (ticket > 0)”的判斷,並進入了判斷框中去執行代碼;
      ②然后它們執行到“Thread.sleep(100);”就睡了;
      ③睡醒后總有一個線程會先搶到cup的執行權,然后執行“ticket--”操作,並將最新的ticket數值推送告知到每個線程;
      ④此時那些在判斷框中的其他的線程並不會再次做“if (ticket > 0)”的判斷,而是直接拿最新的ticket並做“ticket--”操作。
    就算線程在“ticket--”之前每次都做“if (ticket > 0)”的判斷,也依舊會有線程安全問題,因為又可能出現①那種同時通過判斷的狀態。

// volatile的典型用法:檢查某個狀態標記,以判斷是否退出循環。
    volatile boolean asleep;
    ……
    while( !asleep){ countSomeSheep(); }

 

3. volatile的典型用法:檢查某個狀態標記,以判斷是否退出循環。

    volatile boolean asleep;
    ……
    while( !asleep){
        countSomeSheep();
    }

 

4. 總結:因為volatile不能保證變量操作的原子性,所以試圖通過volatile來保證線程安全性是不靠譜的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM