volatile變量能保證線程安全性嗎?爲何?

  在談及線程安全時,常會說到一個變量——volatile。在《Java併發編程實戰》一書中是這麼定義volatile的——Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操做通知到其餘線程。書中的這句話說明了兩點:①volatile變量是一種稍弱的同步機制;②volatile可以確保將變量的更新操做通知到其餘線程——可見性。這兩點和咱們探討「volatile變量是否可以保證線程安全性」息息相關。編程

  什麼是同步機制?在併發程序設計中,各進程對公共變量的訪問必須加以制約,這種制約稱爲同步(該定義源於百度百科)。也就是說,同步機制即爲對共享資源的一種制約。《Java併發編程實戰》中爲何說volatile是一種「稍弱的同步機制」呢?這和volatile可以確保可見性這一重要做用相關:緩存

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

  可見性和「每一個線程都有本身的緩存(或叫「線程的工做內存」)」有關係:併發

    ①操做沒有用volatile來修飾字段時,各個線程都是先從主內存(堆)中複製一份數據到本身的工做內存中,而後操做本身工做內存中的數據,最後再更新到主內存中。測試

    ②當字段被volatile修飾後,各個線程操做該字段時,都是直接在主內存中進行操做的。spa

  由於volatile可以確保可見性,因此,在一些特定情形下可使用 volatile 變量替代鎖,例如:在直接修改變量(不需先判斷再修改)的狀況下,多個線程同時去修改某個變量,一旦某個線程操做成功了,其餘線程對這個變量的修改就馬上創建在最新的變量值上再進行修改,這樣一來就避免了線程安全問題。可是,要使 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)」的判斷,也依舊會有線程安全問題,由於又可能出現①那種同時經過判斷的狀態。code

  總結:當同時知足「①對變量的寫操做不依賴於當前值;②該變量沒有包含在具備其餘變量的不變式中;」這兩個條件時,volatile 變量是可以確保線程安全的。可是,大多數編程情形都會與這兩個條件的其中之一衝突(好比上面的測試代碼中對變量「先判斷後操做」),使得 volatile 變量不能像 synchronized 那樣廣泛適用於實現線程安全。也就是說,大多數狀況下都不能用volatile來確保線程的安全性。blog

相關文章
相關標籤/搜索