Java併發關鍵字Volatile 詳解

Java併發關鍵字Volatile 詳解


  • 問題引出:

    1.Volatile是什麼?java

    2.Volatile有哪些特性?編程

    3.Volatile每一個特性的底層實現原理是什麼?數組

  • 相關內容補充:

  1. 緩存一致性協議:MESI緩存

    ​ 因爲計算機儲存設備(硬盤等)的讀寫速度和CPU的計算速度有着幾個數量級別的差距,爲了避免讓CPU停下來等待讀寫,在CPU和存儲設備之間加了高速緩存,每一個CPU都有本身的高速緩存,並且他們共享同一個主內存區域,當他們都要同步到主內存時,若是每一個CPU緩存裏的數據都不同,這時應該以哪一個數據爲準呢?爲了解決這一同步問題,須要各個處理器都遵循必定的協議,好比MSI,MOSI,MESI等,目前用的比較多的就是MESI協議。多線程

​ 注 :緩存一致性協議是在總線上實現的。併發

MESI是表明了緩存數據的四種狀態,分別是Modified、Exclusive、Shared、Invalid:

​ ①M(Modified):被修改的,處於這一狀態的數據,只在本CPU中有緩存數據,而其餘CPU中沒 有。同時其狀態相對於內存中的值來講,是已經被修改的,且沒有更新到內存中。ide

​ ②E(Exclusive):獨佔的,處於這一狀態的數據,只有在本CPU中有緩存,且其數據沒有修改, 即與內存中一致。性能

​ ③S(Shared):共享的。處於這一狀態的數據在多個CPU中都有緩存,且與內存一致。學習

​ ④I(Invalid):要麼已經不在緩存中,要麼它的內容已通過時。爲了達到緩存的目的,這種狀態 的段將會被忽略。一旦緩存段被標記爲失效,那效果就等同於它歷來沒被加載到緩存中。在 緩存行中有這四種狀態的基礎上,優化

<font color='red'>總結:</font>每一個處理器經過嗅探在總線上傳遞的數據來檢查本身緩存的數據是否過時,當處理器發現本身緩存行數據對應的內存地址被修改,就會將當前緩存行裏的數據設置爲無效。當再次須要使用該數據的時候就會去主內存中從新讀取數據。
  1. Java內存模型:JMM 

    ​ Java內存模型規定了Java變量(實例字段,靜態字段,構成數組對象的元素等,但不包括局部變量和方法參數)存儲到內存和從內存中取出的的底層實現細節,這些變量都存儲在主(Main Memory)中,(主內存只是虛擬機內存的一部分) 每一個線程都有本身的工做內存(Working Memory),工做內存(實際上工做內存並不存在,他只是JMM抽象出來的一個概念)中保存着從主內存讀取來的變量副本拷貝,線程對副本的操做(讀取,修改,賦值)都要在工做內存中進行,而不能直接在主內存中進行,不一樣線程之間也不能訪問彼此的工做內存。且線程之間變量值的傳遞須要通過主內存做爲第三方中介。

  1. 內存間原子性交互操做:

    ①lock(鎖定):做用於主內存上的變量,當一個變量被標識爲Lock的時候,表示該變量是線程 獨佔狀態,此時其餘線程不能夠對該變量進行操做。早前的緩存一致性協議就是這樣,可是這 樣會致使某個變量被一個線程佔用,其餘線程不能夠對其進行訪問,併發就變成了串行,效率 下降,在後來的緩存一致性協議中就拋棄了這種作法。

    ②unlock(解鎖):一樣是做用於主內存中的變量,使變量從鎖定狀態釋放出來,其餘線程纔可 以對其操做。

    ③read(讀取):讀取主內存中的變量,傳輸到線程的工做內存,等待後續的load操做。

    ④load(加載):加載工做內存中的變量,把其放入工做內存的副本變量中。

    ⑤use(使用):把工做內存中的變量副本值傳遞給執行引擎,每當虛擬機遇到使用變量值字節碼 的時候就會進行此操做。

    ⑥assign(賦值):把一個從執行引擎接收到的數據賦給工做內存的變量,即執行賦值操做。

    ⑦store(存儲):把工做內存中通過賦值更新後的值傳遞到主內存中,爲後續write作準備。

    ⑧write(寫入):把store操做傳遞來的值寫入主內存,替換以前的值,完成同步更新。

​ 4.JMM併發的特性要求:

​ ①可見性(Visibility):可見性要求是指當一個線程修改了共享變量的值之後,其餘線程可以馬 上得知這個修改。

​ ②原子性(Atomicity):原子性是指對變量的操做(read,load,assign等上述交互操做)不 可分割不可被打斷,每一個操做都要完整的執行完成才能夠有其餘操做進來。且默認對基本數據類 型的訪問和讀寫都是原子性的(64位的long型和double型會有可能被拆分紅兩個32位進行讀寫 操做,可是這種機率極低,能夠忽略不計。)

​ ③有序性:爲了提高效率,編譯器會對代碼進行亂序優化,而CPU會亂序執行,可是這樣的操做 會致使很嚴重的問題。爲了解決這一問題,使用了內存屏障來防止亂序的發生。 這樣按照順序執 行就是有序性。


進入主題


  • Volatile是什麼?

    Volatile是輕量級的synchronized鎖,所謂輕量級,是由於synchronized使用時會引發線程的上下文切換,使得執行成本更高,效率更低,而Volatile不會有這些問題,效率更高。

  • Volatile特性:

    1.可見性 :Visibility

    ​ ①定義:當一個線程修改了共享變量的值之後,其餘線程可以立刻得知這個修改。

    ​ ②先看一個例子:

    package Test;
    
    public class VolatileTest {
        public static boolean flag = false;
        public static void main(String[] args) throws InterruptedException {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("主線程等待線程A修改數據~~~");
                    while (!flag){}
                    System.out.println("主線程發現數據被線程A修改~~~~");
                }
            }).start();
            Thread.sleep(300);
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    changeData();
                }
            }).start();
    
        }
    
        public static void changeData(){
             flag = true;
            System.out.println("線程A修改完成數據~~~~");
        }
    
    }
    // 執行結果:
    等待線程線程A修改數據~~~
    線程A修改完成數據~~~~

    ​ 能夠看出,當線程A修改完成數據後,另一個線程應該要輸出主線程發現數據被線程A修改~~~~,可是實際的運行狀況是主線程一直處於等待狀態。而若是把public static boolean flag = false;修改成public static volatile boolean flag = false;,也就是把變量用volatile修飾,此時的執行結果:

    等待線程線程A修改數據~~~
    線程A修改完成數據~~~~
    線程A修改數據完成~~~~

    很明顯,線程A修改變量後,主線程也能感知到,使得數據具備可見性,這就是volatile的做用。

    ​ ③volatile可見性底層實現原理:

    ​ 對未加volatile修飾的變量修改時的底層彙編碼:

​ 對volatile修飾的變量修改時的底層彙編碼:

由底層彙編可知,對volatile修飾的變量修改時,彙編指令前面會多一個lock前綴,這個lock 前綴將會致使下面兩件事發生:

​ (1)當即將修改過的數據回寫到主內存中,刷新原來的數據。

在Pentium及Pentium以前的處理器中,帶有lock前綴的指令在執行期間會鎖住總線,使得其餘處理器暫時沒法經過總線訪問內存。很顯然,這會帶來昂貴的開銷。從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎上作了一個頗有意義的優化:若是要訪問的內存區域(area of memory)在lock前綴指令執行期間已經在處理器內部的緩存中被鎖定(即包含該內存區域的緩存行當前處於獨佔或以修改狀態),而且該內存區域被徹底包含在單個緩存行(cache line)中,那麼處理器將直接執行該指令。因爲在指令執行期間該緩存行會一直被鎖定,其它處理器沒法讀/寫該指令要訪問的內存區域,所以能保證指令執行的原子性。這個操做過程叫作緩存鎖定(cache locking),緩存鎖定將大大下降lock前綴指令的執行開銷,可是當多處理器之間的競爭程度很高或者指令訪問的內存地址未對齊時,仍然會鎖住總線。
ps:摘自https://blog.csdn.net/yu280265067/article/details/50986947

​ (2)若是其餘處理器緩存了這個被修改過的數據,那回寫操做會使他們失效。

IA-32處理器和Intel64位處理器使用MESI(緩存一致性)維護內部緩存和其餘處理器緩存的一致性,在多核處理器(多線程)中,處理器和線程使用嗅探技術檢測各自緩存中的數據和總線上傳遞的數據是否一致,若是檢測到有其餘處理器或線程回寫數據,且該數據是共享數據,那麼就會強制使其餘緩存了該數據的緩存中的數據失效。

2.有序性(禁止指令重排序):Odering

​ (1)指令重排序:爲了優化和性能,編譯器和處理器常常會對指令作重排序,且分爲三種。

​ ①編譯器重排序:在不改變單線程程序語義的前提下,從新安排代碼執行順序。

​ ②指令級並行重排序:處理器採用指令級並行技術將多條指令重疊執行,若是數據不存在 依賴,能夠改變機器指令執行。

​ ③內存系統重排序:處理器使用緩存和讀/寫緩衝區,使得加載和存儲看上去是亂序執行。

重排序順序示意圖

​ 先看一個例子:

package Test;

public class NoReoder {
    private static int a = 0;
    private static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                write();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                read();
            }
        }).start();
    }

    public static void write(){
        a = 30;
        flag = true;
        System.out.println("方法一結束~~");
    }

    public static void read(){
        if (flag ) {
            a = a + 10;
            System.out.println(a);
            System.out.println("方法二結束~~");
        }
    }
}

​ 當線程A啓動調用了write方法,線程B啓動調用read方法時能不能知道a被write方法修改了呢?答案是:不必定!!!

因爲write方法中:

a = 30;
flag = true;

這兩個操做的數據沒有依賴性,因此可能會被重排序爲:

flag = true;
a = 30;

這樣就會使得read方法先讀到flag = true ,而 a 還沒修改完,從而使計算結果出錯。

爲了解決這種問題,在JMM中設計了內存屏障技術:

簡單來講,Volatile的有序性就是靠內存屏障來實現,就是把一些操做限制在某些操做以前或者以後,好比將Store操做限制在Load以前,這樣就能讓其餘線程獲得的數據是最新的或者須要先寫入數據再讓其餘線程加載數據。


說在最後:

相關參考,詳見《Java併發編程的藝術》一書

​ 本文僅是對我的學習中一些理解的記錄,鑑於水平有限或多或少存在錯漏或不嚴謹之處,歡迎各位大神批評指正。碼字不易,歡迎轉載轉發但請標註出處。

​ 但願病毒早點結束,再難的日子裏也要堅持學習,新年快樂,最後願工做在與病毒抗爭最前線的醫護人員平安打完這場仗,加油!!!!

相關文章
相關標籤/搜索