併發一:Java內存模型和Volatile

併發一:Java內存模型和Volatile

1、Java內存模型(JMM)

Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和在內存中取出變量的底層細節,是圍繞着在併發過程當中如何處理原子性,可見性和有序性這3個特性創建的java

JMM規則

  1. 變量包含實例字段,靜態字段,構成數組對象的元素,不包含局部變量和方法參數。
  2. 變量都存儲在主內存
  3. 每一個線程都有本身的工做內存,工做內存保存了被該線程使用到的變量的主內存副本拷貝
  4. 線程對變量的全部操做都只能在工做內存,不能直接讀寫主內存的變量
  5. 不一樣線程之間沒法之間訪問對方工做內存中的變量

主內存,線程,工做內存關係圖

定義一個靜態變量:static int a = 1;
線程A工做內存 | 指向 | 主內存 | 操做
-- | -- | -- | --
-- | -- | a = 1 | --
a = 1 | <-- | a = 1 | 線程A拷貝主內存變量副本
a = 3 | -- | a = 1 | 線程A修改工做內存變量值
a = 3 | --> | a = 3 | 線程A工做內存變量存儲到主內存變量數組

上面的一系列內存操做,在JMM中定義了8種操做來完成安全

JMM交互

主內存和工做內存之間的交互,JMM定義了8種操做來完成,每一個操做都是原子性的多線程

  1. lock(鎖定):做用於主內存變量,把一個變量標識爲一條內存獨佔的狀態
  2. unlock(解鎖):做用於主內存變量,把lock狀態的變量釋放出來,釋放出來後才能被其餘線程鎖定
  3. read(讀取):做用於主內存變量,把一個變量的值從主內存傳輸到工做內存中
  4. load(載入):做用於工做內存變量,把read操做的變量放入到工做內存副本中
  5. use(使用):做用於工做內存變量,把工做內存中的變量的值傳遞給執行引擎,每當虛擬機遇到須要這個變量的值的字節碼指令時都執行這個操做
  6. assgin(賦值):做用於工做內存變量,把從執行引擎收到的值賦值給工做內存變量,每當虛擬機遇到須要賦值變量的值的字節碼指令時都執行這個操做
  7. store(存儲):做用於工做內存變量,把工做內存中的一個變量值,傳送到主內存
  8. write(寫入):做用於主內存變量,把store操做的從工做內存取到的變量寫入主內存變量中

2、volatile

當引入線程B的時候
定義一個靜態變量:static int a = 1;
操做順序 | 線程A工做內存 | 線程B工做內存 | 指向 | 主內存 | 操做
-- | -- | -- | -- | -- | --
-- | -- | -- | -- | a = 1 | --
1 | a = 1 | -- | <-- | a = 1 | 線程A拷貝主內存變量副本
2 | a = 3 | -- | -- | a = 1 | 線程A修改工做內存變量值
3 | a = 3 | -- | --> | a = 1 | 線程A工做內存變量存儲到主內存變量,主內存變量還未更新
4.1 | a = 3 | a = 1 | <-- | a = 3 |線程B拷貝主內存變量副本隨後主內存變量更新線程A工做內存變量
4.2 | a = 3 | a = 1 | <-- | a = 3 |線程A工做內存變量存儲到主內存變量隨後線程B獲取主內存變量副本
操做4的時候可能出現:1.線程A變量值還未保存到主內存變量,2.線程A變量值保存到主內存變量。使用volatile關鍵字解決這個問題併發

public static volatile int a = 1;

特性

  1. 保證此變量對全部線程可見,一條線程修改的值,其餘線程對新值能夠當即得知
  2. 禁止指令重排序

可見性

修改內存變量後馬上同步到主內存中,其餘的線程馬上得知得益於Java的先行發生原則app

先行發生原則中的volatile原則:一個volatile變量的寫操做先行於後面發生的這個變量的讀操做,時間順序ide

定義一個靜態變量:static int a = 1;
線程A工做內存 | 線程B工做內存 | 指向 | 主內存 | 操做
-- | -- | -- | -- | --
-- | -- | -- | a = 1 | --
a = 1 | -- | <-- | a = 1 | 線程A拷貝主內存變量副本
a = 3 | -- | -- | a = 1 | 線程A修改工做內存變量值
a = 3 | -- | --> | a = 1 | 線程A工做內存變量存儲到主內存變量
a = 3 | a = 3 | <-- | a = 3 | volatile原則:主內存變量保存線程A工做內存變量操做在線程B工做內存讀取主內存變量操做以前優化

指令重排序和內存屏障

指令重排序:JVM在編譯Java代碼的時候或者CPU在執行JVM字節碼的時候,對現有的指令進行從新排序,目的是爲了再不影響最終結果的前提下,優化程序的執行效率this

內存屏障:一種屏障指令,讓CPU或比編譯器對屏蔽指令以前和以後發出的內存操做執行一個排序約束。
編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。atom

非線程安全

public class VolatileTest implements Runnable {

    public static volatile int num;

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            num++;
        }

    }

    public static void main(String[] args) {
        for(int i = 0; i < 100; i++) {
            VolatileTest t = new VolatileTest();
            Thread t0 = new Thread(t);
            t0.start();
        }
        System.out.println(num);
        
    }
}

這段代碼的結果有可能不是100000,有可能小於100000。
由於num++不是原子操做

使用原則

  1. 運行結果並不依賴變量的當前值,或者可以確保只有單一的線程修改變量的值
  2. 變量不須要與其餘的狀態變量共同參與不變約束

3、原子性、可見性、有序性

原子性

一個操做是不可中斷的。即便是在多個線程一塊兒執行的時候,一個操做一旦開始,就不會被其它線程干擾

原子性保障

  1. synchronized:monitorenter和monitorexit指令
  2. atomic類型:底層爲native方法
  3. 基本數據類型(long,double非原子協定除外)

可見性

當一個線程修改了共享變量的值,其餘線程當即可知

可見性保障

  1. volatile:先行發生(happens-before)原則
  2. synchronized:對一個同步塊unlock以前必須把工做內存變量同步到主內存中
  3. final:final修飾的字段在構造器中初始化完成後,而且構造器沒有把this引用傳遞出去,其餘線程中就能看見final字段值

有序性

程序執行的順序按照代碼的前後順序執行,在Java內存模型中,容許編譯器和處理器對指令進行重排序,可是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性

有序性保障

  1. volatile:先行發生(happens-before)原則
  2. synchronized:同一個時間只能有一個線程得到鎖

先行發生(happens-before)原則

JMM中兩項操做之間的偏序關係,若是操做A發生於操做B以前,操做A發生的影響能夠被操做B觀察到

單線程和正確同步的多線程的執行結果不會被改變

規則

若是兩個操做不在下列規則中,虛擬機能夠對其重排序

  1. 程序次序規則:在一個線程內按控制流順序
  2. 管程鎖定規則:鎖的unlock操做先發生於後面同一個鎖的lock操做
  3. volatile變量規則:一個volatile變量的寫操做先行於後面發生的這個變量的讀操做
  4. 線程啓動規則:start()先發生於此線程的每個操做
  5. 線程終止規則:線程的全部操做都先發生於線程終止操做
  6. 線程中斷規則:對線程interrupt()方法先行於被中斷線程的代碼檢查到中斷事件的發生
  7. 對象終結規則:一個對象初始化完成先發生於它的finalize()方法的開始
  8. 傳遞性:操做A在操做B前,操做B在操做C前,操做A必定在操做C前
相關文章
相關標籤/搜索