volatile變數與普通變數的區別

fulton發表於2017-09-30

我們通常會用volatile實現一些需要執行緒安全的程式碼(也有很多人不敢用,因為不瞭解),但事實上volatile本身並不是執行緒安全的,相對於synchoronized,它有更多的使用侷限性,只能限制在某些特定的場景。本篇文章的目的就是讓大家對 volatile 在本質上有個把握,為了達到這個目的,我們會從java 的記憶體模型及變數操作的記憶體管理來說明(不用怕,你會發現很簡單)。

一、記憶體模型

可以將記憶體簡單分為兩種:工作記憶體和主記憶體。所有的資料最終都需要儲存在主記憶體,工作記憶體是執行緒獨有的,執行緒之間無任何干擾。java的記憶體模型主要就是定義工作記憶體和主記憶體的互動,即工作記憶體如何從主記憶體拷貝資料,以入如何寫資料。java 定義了8種原子性操作來完成工作記憶體與主記憶體的互動:

  • lock 將物件變成執行緒獨佔的狀態
  • unlock 將執行緒獨佔狀態的物件的鎖釋放出來
  • read 從主記憶體讀資料
  • load 將從主記憶體讀取的資料寫入工作記憶體
  • use 工作記憶體使用物件
  • assign 對工作記憶體中的物件進行賦值
  • store 將工作記憶體中的物件傳送到主記憶體當中
  • write 將物件寫入主記憶體當中,並覆蓋舊值

這些操作也是有一定的條件限制的:
read 和load,store和write 必須成對出現,即從主記憶體中讀資料的資料工作記憶體必須接受;傳遞到主記憶體的資料,也不可以被拒絕寫入。
assign後的物件必須回寫到快取
未進行新賦值的物件不允許回寫到主記憶體
新的變數只能在主記憶體產生,且未完成初始化的物件不允許在工作記憶體中使用
物件只允許被一條執行緒鎖定,且可以被此執行緒多次鎖定
未被鎖定的物件不允許執行unlock操作
對一個物件執行unlock之前,必須將物件回寫到主記憶體
java的8種原子性操作,相互之前有一定的約束條件,但並沒有嚴格限制任意兩個操作必須連續出現,只是表示成對出現,這也是為什麼會產生執行緒不安全性的原因。
介紹了上述的背景知識,那我們就來看一下volatile變數到底和普通變數有啥差別吧

二、volatile變數與普通變數

2.1 volatile 的安全性

下面我們用一個例子來說明volatile變數與普通變數的區別。
假設有兩個執行緒操作一個主記憶體的物件,且執行緒1早於執行緒2開始(如下例如示一個a++操作))

public class ThreadSafeTest {
    public static int a = 0;

    public static void increase() {
        a++;
    }


    public static void main (String[] args) {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 100; j++) {
                    increase();
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 100; j++) {
                    increase();
                }
            }
        });

        t1.start();
        t2.start();
    }
}複製程式碼

執行緒2讀取主記憶體物件(a)時,可能發生在幾個時期:read之前、read之後、load之後、use之後、assign之後、 store之後、write之後(如下圖所示);

假設執行緒1執行了a++,a從0變成了1,還未來得及寫回主記憶體物件,執行緒2從主記憶體物件中讀取的資料a=0;此時執行緒1寫入主記憶體a=1,而執行緒2仍然執行完了a++ ,此時仍然 等於1(應該等於2),實際上,這就上相當於執行緒2 讀入了一個過期的資料,導致執行緒不安全。

那如果將a變成volatile物件是否就正確了呢?
volatile對物件的操作做了更嚴格的限制:

  • use之前不進行read和load
  • assign之後必須緊跟store和write
    實際相當於將read load use 三個原子操作變成一個原子操作;將assign-store-write變成一個原子操作。很多文章上都講volatile對所有的執行緒是可見的,指的就是執行完了assign之後立即就會回寫主記憶體;在任意一個執行緒讀取主記憶體物件時,都會重新整理主記憶體。在主記憶體中表現是資料一致性的,但是各執行緒記憶體當中卻不一定是一致性的。
    同樣是上面的程式碼,換成volatile
    ```
    public class ThreadSafeTest {
    public static volatile int a = 0;

    public static void increase() {

      a++;複製程式碼

    }

public static void main (String[] args) {

    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int j = 0; j < 100; j++) {
                increase();
            }
        }
    });

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int j = 0; j < 100; j++) {
                increase();
            }
        }
    });

    t1.start();
    t2.start();

}複製程式碼

}

```
執行後發現,也拿不到正確的結果(如果拿到請把j的數值調大)。操你媽,不是說是執行緒安全的變數嗎?為啥也不正確?
這是因為執行緒內部的資料仍然有可能存在不一致性,比如,如果執行緒2讀取資料時,處線上程1use之後,但執行緒1此時還未來得及回寫主快取,這時候執行緒2使用到的資料仍然是0,兩個執行緒同時對0++,得到的結果只會是1,而不是理想中的2。

2.2 volatile 的執行緒安全是有條件的

即然volatile 是非執行緒安全的,那要它還有什麼用呢?如果你看過我寫過的“執行緒安全”的文章應該知道,所有的物件都是相對執行緒安全的,也就是有條件的。volatile的執行緒安全當然也是有條件的,它是對synchronized這一重量級執行緒同步的一種補充,其整體效能上優於synchronized。那volatile的執行緒安全的條件是什麼呢?適合使用在哪些場景?
《java虛擬機器》給出兩個條件:

  • 運算結果並不依賴變數的當前值(即結果對產生中間結果不依賴),或者能夠確保只有單一的執行緒修改變數的值
  • 變數不需要與其它的狀態變數共同參與不變約束(我認為此條多此一舉,這個其它變數也必須得是執行緒安全的才行)

那適合哪些場景呢?這個我就不一一舉例了,一個哥門總結得很好,參考如下:www.ibm.com/developerwo…

相關文章