Java併發(六):volatile的實現原理

synchronized是一個重量級的鎖,volatile一般被比喻成輕量級的synchronizedhtml

volatile是一個變量修飾符,只能用來修飾變量。數據庫

volatile寫:當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存。緩存

volatile讀:當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。併發

volatile實現原理

1)JMM把內存屏障指令分爲下列四類:post

StoreLoad Barriers是一個「全能型」的屏障,它同時具備其餘三個屏障的效果。現代的多處理器大都支持該屏障(其餘類型的屏障不必定被全部處理器支持)。執行該屏障開銷會很昂貴,由於當前處理器一般要把寫緩衝區中的數據所有刷新到內存中(buffer fully flush)。優化

Store:數據對其餘處理器可見(即:刷新到內存)url

Load:讓緩存中的數據失效,從新從主內存加載數據 spa

2)JMM針對編譯器制定的volatile重排序規則表線程

是否能重排序 第二個操做
第一個操做 普通讀/寫 volatile讀 volatile寫
普通讀/寫     NO
volatile讀 NO NO NO
volatile寫   NO NO

 

 

 

 

 

 

舉例來講,第三行最後一個單元格的意思是:在程序順序中,當第一個操做爲普通變量的讀或寫時,若是第二個操做爲volatile寫,則編譯器不能重排序這兩個操做。code

從上表咱們能夠看出:

  • 當第二個操做是volatile寫時,無論第一個操做是什麼,都不能重排序。這個規則確保volatile寫以前的操做不會被編譯器重排序到volatile寫以後。
  • 當第一個操做是volatile讀時,無論第二個操做是什麼,都不能重排序。這個規則確保volatile讀以後的操做不會被編譯器重排序到volatile讀以前。
  • 當第一個操做是volatile寫,第二個操做是volatile讀時,不能重排序。

JMM內存屏障插入策略(編譯器能夠根據具體狀況省略沒必要要的屏障):

  • 在每一個volatile寫操做的前面插入一個StoreStore屏障。
    •  對於這樣的語句Store1 StoreStore Store2,在Store2及後續寫入操做執行前,保證Store1的寫入操做對其它處理器可見
  • 在每一個volatile寫操做的後面插入一個StoreLoad屏障。
    • 對於這樣的語句Store1 StoreLoad Load2,在Load2及後續全部讀取操做執行前,保證Store1的寫入對全部處理器可見
  • 在每一個volatile讀操做的後面插入一個LoadLoad屏障。
    • 對於這樣的語句Load1 LoadLoad Load2,在Load2及後續讀取操做要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢
  • 在每一個volatile讀操做的後面插入一個LoadStore屏障。  
    • 對於這樣的語句Load1 LoadStore Store2,在Store2及後續寫入操做被刷出前,保證Load1要讀取的數據被讀取完畢

volatile保證可見性

  volatile修飾的變量寫以後將本地內存刷新到主內存,保證了可見性

volatile保證有序性

  volatile變量讀寫先後插入內存屏障以免重排序,保證了有序性

volatile不保證原子性

  volatile不是鎖,與原子性無關

  要我說,因爲CPU按照時間片來進行線程調度的,只要是包含多個步驟的操做的執行,自然就是沒法保證原子性的。由於這種線程執行,又不像數據庫同樣能夠回滾。若是一個線程要執行的步驟有5步,執行完3步就失去了CPU了,失去後就可能不再會被調度,這怎麼可能保證原子性呢。

爲何synchronized能夠保證原子性 ,由於被synchronized修飾的代碼片斷,在進入以前加了鎖,只要他沒執行完,其餘線程是沒法得到鎖執行這段代碼片斷的,就能夠保證他內部的代碼能夠所有被執行。進而保證原子性。

(摘自http://www.hollischuang.com/archives/2673)

volatile不保證原子性的例子:

/**
 * 建立10個線程,而後分別執行1000次i++操做。目的是程序輸出結果10000
 * 可是,屢次執行的結果都小於10000。這其實就是volatile沒法知足原子性的緣由。
 */
public class Test {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for (int i = 0; i < 10; i++) {
            new Thread() {
                public void run() {
                    for (int j = 0; j < 1000; j++)
                        test.increase();
                };
            }.start();
        }

        while (Thread.activeCount() > 1)
            // 保證前面的線程都執行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

 

volatile的內存屏障插入策略很是保守,其實在實際中,只要不改變volatile寫-讀得內存語義,編譯器能夠根據具體狀況優化,省略沒必要要的屏障。參考:【死磕Java併發】—–Java內存模型之分析volatile

 

參考資料

深刻理解Java中的volatile關鍵字

Java併發(一):Java內存模型乾貨總結

【死磕Java併發】—–深刻分析volatile的實現原理

再有人問你volatile是什麼,把這篇文章也發給他。

相關文章
相關標籤/搜索