天天看點

【十五】Java多線程之volatile(可見性、有序性、happens-before、記憶體屏障和禁止重排序)

一、簡介

volatile能保證可見性、有序性。

synchronize與volatile的差別就是,synchronize能保證原子性,而volatile不能。

特殊情況下可以保證原子性(比如long,64位,先讀前32位再讀後32位,如果這個long變量用volatile修飾就能保證原子性)。

它靠記憶體屏障和禁止重排序來實作可見性、有序性。

二、可見性

1.導緻共享變量線上程間不可見的原因:

  1. 線程交叉執行
  2. 重排序結合線程交叉執行
  3. 共享變量更新後的值沒有線上程工作記憶體和主存之間及時更新

2.可見性(synchronize、final、volatile可以保證可見性)

這篇隻說volatile,synchronize在其他篇章聊。

可見性是說:一個線程對main memory的修改可以及時的被其他線程觀察到。

被volatile修飾的變量能夠保證每個線程能夠擷取該變量的最新值,進而避免出現資料髒讀的現象。

java記憶體模型

【十五】Java多線程之volatile(可見性、有序性、happens-before、記憶體屏障和禁止重排序)

volatile修飾的共享變量保證可見性和有序性的原因: 

1.它将目前cpu cache  memory行的資料寫回main memory

2.這個寫回main memory的操作會使得其他CPU裡緩存了該記憶體位址的資料無效

3.其他線程用到這個變量的時候會直接從main memory中讀取,而不是使用cpu cache memory中的備份。

 而普通的共享變量不能保證可見性,因為普通共享變量被修改之後,什麼時候被寫入主存是不确定的,當其他線程去讀取時,此時記憶體中可能還是原來的舊值,是以無法保證可見性。

三、有序性(synchronize和volatile可以保證有序性)

有序性:一個線程觀察其他線程中的指令執行順序,由于指令重排序的存在,該觀察結果一般是雜亂無序的。

在Java記憶體模型中,允許編譯器和處理器對指令進行重排序,重排序過程不會影響到單線程程式的執行,但會影響到多線程并發執行的正确性。

java記憶體模型的先天有序性happens-before原則

JSR-133中定義了如下的 happens-before 規則:

  1. 單一線程原則:在一個線程内,程式前面的操作先于後面的操作。
  2. 螢幕鎖規則:一個unlock操作先于後面對同一個鎖的lock操作發生。
  3. volatile變量規則:對一個 volatile 變量的寫操作先行發生于後面對這個變量的讀操作,也就是說讀取的值肯定是最新的。
  4. 線程啟動規則:Thread對象的start()方法調用先行發生于此線程的每一個動作。
  5. 線程加入規則:Thread 對象的結束先行發生于 join() 方法傳回。
  6. 線程中斷規則:對線程 interrupt() 方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生,可以通過 interrupted() 方法檢測到是否有中斷發生。
  7. 對象終結規則:一個對象的初始化完成(構造函數執行結束)先行發生于它的 finalize() 方法的開始。
  8. 傳遞性:如果操作 A 先行發生于操作 B,操作 B 先行發生于操作 C,那麼操作 A 先行發生于操作 C。

如果兩個操作執行順序無法從happens-before原則推導出來,那麼他們就不能保證有序性,jvm可以随意的對他們進行重排序。

四、記憶體屏障和禁止重排序

JMM四類記憶體屏障

屏障類型 指令示例 說明
LoadLoad Barriers Load1;LoadLoad;Load2 確定Load1資料的裝載先于Load2及所有後續裝載指令
StoreStore Barriers Store1;StoreStore;Store2 確定Store1資料重新整理到記憶體,先于Store2及所有後續存儲指令
LoadStore Barriers Load1;LoadStore;Store2 確定Load1資料裝載先Store2及所有後續存儲指令
StoreLoad Barriers Store1;StoreLoad;Load2 確定Store1資料重新整理到記憶體,先于Load2及所有後續裝載。StoreLoad Barriers會使該屏障之前的所有記憶體通路指令(存儲和裝載)完成之後,才執行該屏障之後的記憶體通路指令。

 JMM會針對編譯器制定volatile重排序規則

是否重排序 第二步
第一步 普通讀/寫 volatile讀 volatile寫
普通讀/寫 NO
volatile讀 NO NO NO
volatile寫 NO NO

 編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定類型的處理器重排序

  1. 在每個volatile寫操作的前面插入一個StoreStore屏障;
  2. 在每個volatile寫操作的後面插入一個StoreLoad屏障;
  3. 在每個volatile讀操作的後面插入一個LoadLoad屏障;
  4. 在每個volatile讀操作的後面插入一個LoadStore屏障。

volatile寫插入屏障示例:

【十五】Java多線程之volatile(可見性、有序性、happens-before、記憶體屏障和禁止重排序)

volatile讀插入屏障示例: 

【十五】Java多線程之volatile(可見性、有序性、happens-before、記憶體屏障和禁止重排序)

五、常用場景

1.狀态标記量

volatile boolean inited = false;
//線程1:
context = loadContext();  
inited = true;            
 
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
           

因為context=loadContext()和inited=true之間的執行順序不能保證(不符合happens-before中的任何一條),是以inited變量要用volatile修飾,以免出現這種情況:

線程1在context=loadContext()方法執行之前就先執行了inited=true,此時context根本沒有加載

而,線程2恰好在此時判斷出inited=true,就去執行doSomethingwithconfig(context), 此時context根本沒有加載

2.雙重檢查

/**
 * 懶漢模式 -》 雙重同步鎖單例模式
 * 單例執行個體在第一次使用時進行建立
 */

public class SingletonExample4 {

    // 私有構造函數
    private SingletonExample4() {

    }

    // 1、memory = allocate() 配置設定對象的記憶體空間
    // 2、ctorInstance() 初始化對象
    // 3、instance = memory 設定instance指向剛配置設定的記憶體

    // JVM和cpu優化,發生了指令重排

    // 1、memory = allocate() 配置設定對象的記憶體空間
    // 3、instance = memory 設定instance指向剛配置設定的記憶體
    // 2、ctorInstance() 初始化對象

    // 單例對象  volatile + 雙重檢測機制 -> 禁止指令重排
    private volatile static SingletonExample4 instance = null;

    // 靜态的工廠方法
    public static SingletonExample4 getInstance() {
        if (instance == null) { // 雙重檢測機制        // B
            synchronized (SingletonExample4.class) { // 同步鎖
                if (instance == null) {
                    instance = new SingletonExample4(); // A - 3
                }
            }
        }
        return instance;
    }
}
           

繼續閱讀