深刻理解Volatile

轉自:https://juejin.im/editor/drafts/5acda6976fb9a028d937821f


一旦一個共享變量(類的成員變量、 類的靜態成員變量) 被 volatile 修飾以後, 那麼就具有了兩層語義:

  1. 保證了不一樣線程對這個變量進行讀取時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。 (volatile 解決了線程間共享變量的可見性問題)。編程

  2. 禁止進行指令重排序, 阻止編譯器對代碼的優化。緩存

內存可見性:

  • 第一: 使用 volatile 關鍵字會強制將修改的值當即寫入主存;bash

  • 第二: 使用 volatile 關鍵字的話, 當線程 2 進行修改時, 會致使線程 1 的工做內存中緩存變量 stop 的緩存行無效(反映到硬件層的話, 就是 CPU 的 L1或者 L2 緩存中對應的緩存行無效) ;多線程

  • 第三: 因爲線程 1 的工做內存中緩存變量 stop 的緩存行無效, 因此線程 1再次讀取變量 stop 的值時會去主存讀取。ide

那麼, 在線程 2 修改 stop 值時(固然這裏包括 2 個操做, 修改線程 2 工做內存中的值, 而後將修改後的值寫入內存) , 會使得線程 1 的工做內存中緩存變量 stop 的緩存行無效, 而後線程 1 讀取時, 發現本身的緩存行無效, 它會等待緩存行對應的主存地址被更新以後, 而後去對應的主存讀取最新的值。性能

具體內容參考個人另一篇博客:優化

禁止重排序:

volatile 關鍵字禁止指令重排序有兩層意思:ui

  • 當程序執行到 volatile 變量的讀操做或者寫操做時, 在其前面的操做的更改確定所有已經進行, 且結果已經對後面的操做可見; 在其後面的操做確定尚未進行this

  • 在進行指令優化時, 不能把 volatile 變量前面的語句放在其後面執行,也不能把 volatile 變量後面的語句放到其前面執行。spa

爲了實現 volatile 的內存語義, 加入 volatile 關鍵字時, 編譯器在生成字節碼時,會在指令序列中插入內存屏障, 會多出一個 lock 前綴指令。 內存屏障是一組處理器指令, 解決禁止指令重排序和內存可見性的問題。 編譯器和 CPU 能夠在保證輸出結果同樣的狀況下對指令重排序, 使性能獲得優化。 處理器在進行重排序時是會考慮指令之間的數據依賴性。

內存屏障, 有 2 個做用:

  • 1.先於這個內存屏障的指令必須先執行, 後於這個內存屏障的指令必須後執行。

  • 2.使得內存可見性。 因此, 若是你的字段是 volatile, 在讀指令前插入讀屏障, 可讓高速緩存中的數據失效, 從新從主內存加載數據。 在寫指令以後插入寫屏障, 能讓寫入緩存的最新數據寫回到主內存。

Lock 前綴指令在多核處理器下會引起了兩件事情:

  1. 將當前處理器中這個變量所在緩存行的數據會寫回到系統內存。 這個寫回內存的操做會引發在其餘 CPU 裏緩存了該內存地址的數據無效。 可是就算寫回到內存, 若是其餘處理器緩存的值仍是舊的, 再執行計算操做就會有問題, 因此在多處理器下, 爲了保證各個處理器的緩存是一致的, 就會實現緩存一致性協議, 每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了, 當處理器發現本身緩存行對應的內存地址被修改, 就會將當前處理器的緩存行設置成無效狀態, 當處理器要對這個數據進行修改操做的時候, 會強制從新從系統內存裏把數據讀處處理器緩存裏。

  2. 它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置, 也不會把前面的指令排到內存屏障的後面; 即在執行到內存屏障這句指令時, 在它前面的操做已經所有完成。

深刻理解緩存一致性問題:

當程序在運行過程當中, 會將運算須要的數據從主存複製一份到 CPU 的高速緩存當中, 那麼 CPU 進行計算時就能夠直接從它的高速緩存讀取數據和向其中寫入數據, 當運算結束以後, 再將高速緩存中的數據刷新到主存當中。 舉個簡單的例子, 好比下面的這段代碼:

i = i+1複製代碼

當線程執行這個語句時, 會先從主存當中讀取 i 的值, 而後複製一份到高速緩存當中, 而後 CPU 執行指令對 i 進行加 1 操做, 而後將數據寫入高速緩存,最後將高速緩存中 i 最新的值刷新到主存當中。這個代碼在單線程中運行是沒有任何問題的, 可是在多線程中運行就會有問題了。 在多核 CPU 中, 每條線程可能運行於不一樣的 CPU 中, 所以每一個線程運行時有本身的高速緩存(對單核 CPU 來講, 其實也會出現這種問題, 只不過是以線程調度的形式來分別執行的) 。

本文咱們以多核 CPU 爲例好比同時有 2 個線程執行這段代碼, 假如初始時 i 的值爲 0, 那麼咱們但願兩個線程執行完以後 i 的值變爲 2。 可是事實會是這樣嗎?

可能存在下面一種狀況: 初始時, 兩個線程分別讀取 i 的值存入各自所在的CPU 的高速緩存當中, 而後線程 1 進行加 1 操做, 而後把 i 的最新值 1 寫入到內存。 此時線程 2 的高速緩存當中 i 的值仍是 0, 進行加 1 操做以後, i 的值爲1, 而後線程 2 把 i 的值寫入內存。最終結果 i 的值是 1, 而不是 2。 這就是著名的緩存一致性問題。 一般稱這種被多個線程訪問的變量爲共享變量。也就是說, 若是一個變量在多個 CPU 中都存在緩存(通常在多線程編程時纔會出現) , 那麼就可能存在緩存不一致的問題。

如何解決緩存一致性的問題:爲了解決緩存不一致性問題, 一般來講有如下 2 種解決方法:1) 經過在總線加 LOCK#鎖的方式2) 經過緩存一致性協議

經過在總線加 LOCK#鎖的方式:在早期的 CPU 當中, 是經過在總線上加 LOCK#鎖的形式來解決緩存不一致的問題。 由於 CPU 和其餘部件進行通訊都是經過總線來進行的, 若是對總線加 LOCK#鎖的話, 也就是說阻塞了其餘 CPU 對其餘部件訪問(如內存) ,從而使得只能有一個 CPU 能使用這個變量的內存。 好比上面例子中 若是一個線程在執行 i = i +1, 若是在執行這段代碼的過程當中, 在總線上發出了 LCOK#鎖的信號, 那麼只有等待這段代碼徹底執行完畢以後, 其餘 CPU 才能從變量 i所在的內存讀取變量, 而後進行相應的操做。 這樣就解決了緩存不一致的問題。可是上面的方式會有一個問題, 因爲在鎖住總線期間, 其餘 CPU 沒法訪問內存, 致使效率下。

可是上面的方式會有一個問題, 因爲在鎖住總線期間, 其餘 CPU 沒法訪問內存, 致使效率低下。

經過緩存一致性協議:因此就出現了緩存一致性協議。 該協議保證了每一個緩存中使用的共享變量的副本是一致的。

它核心的思想是: 當 CPU 向內存寫入數據時, 若是發現操做的變量是共享變量, 即在其餘 CPU 中也存在該變量的副本, 會發出信號通知其餘 CPU 將該變量的緩存行置爲無效狀態, 所以當其餘 CPU 須要讀取這個變量時, 發現本身緩存中緩存該變量的緩存行是無效的, 那麼它就會從內存從新讀取。

內存屏障能夠被分爲如下幾種類型:

  • LoadLoad 屏障: 對於這樣的語句 Load1; LoadLoad; Load2, 在 Load2 及後續讀取操做要讀取的數據被訪問前, 保證 Load1 要讀取的數據被讀取完畢。

  • StoreStore 屏障: 對於這樣的語句 Store1; StoreStore; Store2, 在 Store2 及後續寫入操做執行前, 保證 Store1 的寫入操做對其它處理器可見。

  • LoadStore 屏障: 對於這樣的語句 Load1; LoadStore; Store2, 在 Store2 及後續寫入操做被刷出前, 保證 Load1 要讀取的數據被讀取完畢。

  • StoreLoad 屏障: 對於這樣的語句 Store1; StoreLoad; Load2, 在 Load2 及後續全部讀取操做執行前, 保證 Store1 的寫入對全部處理器可見。 它的開銷是四種屏障中最大的。 在大多數處理器的實現中, 這個屏障是個萬能屏障, 兼具其它三種內存屏障的功能。

這裏用一張圖來詳細分析指令的執行順序:


StoreStore屏障能夠保證在volatile寫以前,其前面的全部普通寫操做已經對任意處理器可見了,由於StoreStore屏障將保障上面全部的普通寫在volatile寫以前刷新到主內存

x86處理器僅僅會對寫-讀操做作重排序

所以會省略掉讀-讀、讀-寫和寫-寫操做作重排序的內存屏障

在x86中,JMM僅需在volatile後面插入一個StoreLoad屏障便可正確實現volatile寫-讀的內存語義

這意味着在x86處理器中,volatile寫的開銷比volatile讀的大,由於StoreLoad屏障開銷比較大

對於Volatile的使用,附上代碼講解:

/* 
 * 1、volatile 關鍵字:當多個線程進行操做共享數據時,能夠保證內存中的數據可見。 
 * 相較於 synchronized 是一種較爲輕量級的同步策略。 
 * 注意: 
 * 1. volatile 不具有「互斥性」 
 * 2. volatile 不能保證變量的「原子性」 
 */  
 public class TestVolatile {//main線程和ThreadDemo線程對flag屬性的可見性問題  
​
    public static void main(String[] args) {  
        ThreadDemo td = new ThreadDemo();  
        new Thread(td).start();  
​
        while(true){  
            if(td.isFlag()){  
                System.out.println("------------------");  
                break;  
            }  
        }  
​
    }  
​
}  
​
class ThreadDemo implements Runnable {  
​
    //不添加volatile將不會輸出--------------------  
    private volatile boolean flag = false;  
      
    @Override  
    public void run() {  
      
        try {  
            Thread.sleep(200);  
        } catch (InterruptedException e) {  
        }  
      
        flag = true;  
      
        System.out.println("flag=" + isFlag());  
      
    }  
      
    public boolean isFlag() {  
        return flag;  
    }  
      
    public void setFlag(boolean flag) {  
        this.flag = flag;  
    }  
}  複製代碼
相關文章
相關標籤/搜索