Java多執行緒之初識volatile

innoyiya發表於2018-01-31

在討論volatile之前我們先來了解下cpu與記憶體之間的關係:

Java多執行緒之初識volatile

手殘黨、圖醜、大家心中有個大概就行了。

圖中的快取為cpu快取,實際上電腦一般設有三級快取。cpu快取為於cpu和記憶體之間的臨時儲存器,它的容量很小但交換速度卻比記憶體快得多。

快取的出現主要是為了解決CPU運算速度與記憶體讀寫速度不匹配的矛盾,因為CPU運算速度要比記憶體讀寫速度快很多,這樣會使CPU花費很長時間等待資料到來或把資料寫入記憶體。具體大家可自行百度。

volatile提供的三個特性:

  • 原子性:

    一個很好的例子是:32位機上的long型別讀寫操作是分為高低位讀寫兩次的,並且不原子性。所謂原子性是指一個集合操作中所有操作“同生共死”、要麼一起成功執行要麼一起不執行。

    long i = 0; i = 10;

    當有一條執行緒執行到i = 10時,首先會為低16位進行賦值,倘若此時有另一條執行緒來讀取時只會讀到只賦值的低16位的資料,從而造成bug的出現。 不過volatile並不能代替鎖,它無法保證複合操作的原子性,例如:i++,實際上此操作是由三個步驟組成的:首先取得i的值,再對i進行+1,再將結果寫回i。

  • 可見性:

    就如上圖所示,每個CPU都有屬於自己的快取

    int i = 0;
    執行緒1執行的程式碼
    i = 10;
    
    執行緒2執行的程式碼
    j = i;
    複製程式碼

    假設執行緒1先於執行緒2執行,並且CPU1執行執行緒1、CPU2執行執行緒2。

    當執行緒1執行 i =10這句時,會先把i的初始值載入到CPU1的快取記憶體中,然後賦值為10,那麼在CPU1的快取記憶體當中i的值變為10了,卻沒有立即寫入到記憶體當中。

  此時執行緒2執行 j = i,它會先去主存讀取i的值並載入到CPU2的快取當中,注意此時記憶體當中i   的值還是0,那麼就會使得j的值為0,而不是10。

  這就是可見性問題,執行緒1對變數i修改了之後,執行緒2沒有立即看到執行緒1修改的值。      當一個共享變數被volatile修飾時,它會保證修改的值會立即被更新到記憶體中,當有其他執行緒需   要讀取時,它會去記憶體中讀取新值。

  • 有序性:

    大部分人會認為程式執行順序會和程式碼編寫順序一樣,其實不然。在JMM記憶體模型中允許編譯器和處理器對指令進行重新排序來進行優化、以便於CPU能夠並行執行指令,從而提高效率。

    在單執行緒中重排序的結果不會影響程式的執行結果,但卻會影響多執行緒並行執行的正確性了。 舉個簡單的栗子:

    Thread 1    Thread 2
    1:r2 = A    3:r1 = b
    2:b = 1     4:A = 2
    複製程式碼

    從順序上看 (r2 == 2) 、(r1 == 1) 應該不可能出現,但如果被重排序成下列順序是就不一定了:

    Thread 1    Thread 2
    b = 1       r1 = b
    r2 = A      A = 2
    複製程式碼

    再舉一個稍微複雜的例子:

    class order {
        int a = 0;
        boolean flag = false;
        public void writer() {
            a = 1;
            flag = true;
        }
        public void reader() {
            if (flag) {
                int i = a + 1;
                dosomething;
            }
        }
    }
    複製程式碼

    假設執行緒A先執行writer()方法,然後執行緒B執行reader()方法。當發生指令重排序後,writer()方法中的flag的寫入可能會先於a的寫入,造成執行緒B在執行reader方法時判斷正確併為i賦值。

    指令重排序帶來的可見性問題

    雖然指令重排序會帶來許多問題,但卻能有效提高效率,並且在序列程式碼中大家可放心:

    指令重排序可以保證序列語義一致,但沒有義務保證多執行緒之間的語義也一致。

Java虛擬機器還規定了些規則指定了哪些指令不能重排序

Happen-Before 規則:

  • 程式順序原則:一個執行緒內保證語義的序列性
  • volatile規則:volatile變數的寫,先傳送於讀,保證volatile變數的可見性
  • 鎖規則:解鎖必然先發生於隨後的加鎖前
  • 傳遞性:A先於B,B先於C,那麼A必然先於C
  • 執行緒的start()方法先於它的每一個動作
  • 執行緒的中斷先於被中斷程式的程式碼
  • 物件的建構函式的執行,結束先於finalized()方法

總的來說,volatile還涉及到JMM記憶體模型等相關知識。推薦大家去看《Java併發程式設計的藝術》和《Java高併發程式設計》,裡面講解更加透徹。

相關文章