1. 程式人生 > >併發程式設計之JMM解決執行緒安全問題及volatile關鍵字--02

併發程式設計之JMM解決執行緒安全問題及volatile關鍵字--02

文章目錄

1. JMM解決原子性、可見性、有序性的問題

  在Java中提供了一系列和併發處理相關的關鍵字,比如volatile、Synchronized、final、juc(java.util.concurrent)等,這些就是Java記憶體模型封裝了底層的實現後提供給開發人員使用的關鍵字,在開發多執行緒程式碼的時候,我們可以直接使用synchronized等關鍵詞來控制併發,使得我們不需要關心底層的編譯器優化、快取一致性的問題了,所以在Java記憶體模型中,除了定義了一套規範,還提供了開放的指令在底層進行封裝後,提供給開發人員使用。

1.1 原子性保障

  在java中提供了兩個高階的位元組碼指令monitorenter和monitorexit,在Java中對應的Synchronized

來保證程式碼塊內的操作是原子的

1.2 可見性

  Java中的volatile關鍵字提供了一個功能,那就是被其修飾的變數在被修改後可以立即同步到主記憶體,被其修飾的變數在每次是用之前都從主記憶體重新整理。因此,可以使用volatile來保證多執行緒操作時變數的可見性。

  除了volatile,Java中的synchronized和final兩個關鍵字也可以實現可見性.

1.3 有序性

  在Java中,可以使用synchronized和volatile來保證多執行緒之間操作的有序性。實現方式有所區別:volatile關鍵字會禁止指令重排。synchronized關鍵字保證同一時刻只允許一條執行緒操作。

2. volatile如何保證可見性

指令檢視方法:
步驟一:下載hsdis工具 ,https://sourceforge.net/projects/fcml/files/fcml-1.1.1/hsdis-1.1.1-win32-amd64.zip/download

步驟二:解壓後存放到jre目錄的server路徑下

步驟三:然後跑main函式,跑main函式之前,加入如下虛擬機器引數:
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -
XX:CompileCommand=compileonly,*App.getInstance(替換成實際執行的程式碼)

  volatile變數修飾的共享變數,在進行寫操作的時候會多出一個lock字首的彙編指令,這個指令會觸發匯流排鎖或者快取鎖,通過快取一致性協議來解決可見性問題

1.保證可見性:

  • .將當前處理器快取行的資料寫回到系統記憶體
  • 這個寫回的記憶體操作會使其他CPU裡快取了該記憶體地址的資料無效。

 如下:當執行緒A寫入主記憶體後,讓執行緒B本地記憶體x值無效,讓執行緒B重新去主記憶體讀取資料
在這裡插入圖片描述

3. volatile防止指令重排序

  指令重排的目的是為了最大化的提高CPU利用率以及效能,CPU的亂序執行優化在單核時代並不影響正確性,但是在多核時代的多執行緒能夠在不同的核心上實現真正的並行,一旦執行緒之間共享資料,就可能會出現一些不可預料的問題。

指令重排序必須要遵循的原則是,不影響程式碼執行的最終結果,編譯器和處理器不會改變存在資料依賴關係的兩個操作的執行順序,(這裡所說的資料依賴性僅僅是針對單個處理器中執行的指令和單個執行緒中執行的操作.)這個語義,實際上就是as-if-serial語義,不管怎麼重排序,單執行緒程式的執行結果不會改變,編譯器、處理器都必須遵守as-if-serial語義

3.1 記憶體屏障

  記憶體屏障需要兩個問題,一個是編譯器的優化亂序和CPU的執行亂序,我們可以分別使用優化屏障和記憶體屏障這兩個機制來解決

1)CPU層面的亂序優化
  CPU的亂序執行,本質還是,由於在多CPU的機器上,每個CPU都存在cache,當一個特定資料第一次被特定一個CPU獲取時,由於在該CPU快取中不存在,就會從記憶體中去獲取,被載入到CPU快取記憶體中後就能從快取中快速訪問。當某個CPU進行寫操作時,它必須確保其他的CPU已經將這個資料從他們的快取中移除,這樣才能讓其他CPU安全的修改資料。顯然,存在多個cache時,我們必須通過一個cache一致性協議來避免資料不一致的問題,而這個通訊的過程就可能導致亂序訪問的問題,也就是執行時的記憶體亂序訪問。

  CPU層面的記憶體屏障是在x86的cpu中,實現了相應的記憶體屏障:
寫屏障(store barrier)、讀屏障(load barrier)和全屏障(Full Barrier),主要的作用是 防止指令之間的重排序、 保證資料的可見性。細節略。

2)編譯器層面指令重排序
 JMM通過如下記憶體屏障指令來禁止特定型別的處理器重排序,volatile就包含有這個指令,分類如下所示:
在這裡插入圖片描述

  在編譯器層面,通過volatile關鍵字,取消編譯器層面的快取和重排序。保證編譯程式時在優化屏障之前的指令不會在優化屏障之後執行。這就保證了編譯時期的優化不會影響到實際程式碼邏輯順序。
如果硬體架構本身已經保證了記憶體可見性,那麼volatile就是一個空標記,不會插入相關語義的記憶體屏障。如果硬體架構本身不進行處理器重排序,有更強的重排序語義,那麼volatile就是一個空標記,不會插入相關語義的記憶體屏障。

 在volatile欄位讀或者寫的語句中,它前面的資料必定發生在當前語句之前,前面的語句可以發生重排序;同理,在volatile欄位操作之後也同樣必須發生在這個語句之後,後面的不會管他們的重排序情況。

例子:

int a,b,c ;
 
volatile int v1=1;
 
volatile int v2 =2;
 
void readAndWrite(){
   a =1;       //1
   b=2;        //2
   c=3;        //3
   int i =v1;   //4 第一個volatile讀
   int j =v2;    //5 第二個volatile讀
   a = i+j;      // 6 普通寫
   b = i+j;       //7
   c = i+j;       //8
   v1=i+1;       //9 第一個volatile寫
   v2=j* 2;      //10 第二個volatile寫
}

如上所示:1,2,3可能發生重排序,但是在4的時候,一定會保證1,2,3都是執行了的。同理5,6,都是volatile欄位操作都會保證前面的都是執行了的不會讓一塊發生重排序;同理6,7,8可能發生重排序;

4. volatile原子性問題

volatile不能保證資料的原子性問題, 可以在java編譯後 通過javap -c Demo.class,去檢視位元組碼指令

例子一:

//我們通過下面一個例子,對一個通過volatile修飾的值進行遞增
public class Demo {
  volatile int i;
  public void incr(){
    i++; //這裡有三個步驟,不能保證這個操作的原子性,會分為三個步驟:1.讀取volatile變數的值到local;2.增加變數的值;3.把local的值寫回讓其他執行緒可見
 }
  public static void main(String[] args) {
    new Thread(()->{
		new Demo().incr();
	});
	int j =i; //讀操作,這個可以保證前後指令的排序問題 ,具有原子性概念
	i =3;  //寫操作,對其他記憶體是可見的,具有原子性概念
 }
}

例子二:
使用volatile實現單例模式

Pubulic Class Singleton {
 
    private volatile static Singleton instance; //1 這裡用volatile申明
 
    public static Singleton getInstance() {
        if (null == instance) {
            synchronized (Singleton.class) {
                if (null == instance) {
                    instance = new Singleton ();  //2 這裡有三個操作,可能會被重排序
                }
            }
        }
        return instance;
    }

如上所示,單例的引用採用volatile申明,就是為了避免2處的重排序

問題解析:
instance = new Singleton() 實際是由下面三步完成的:

  • memory=allocate();    1. //分配物件記憶體空間
  • ctorInstance(memory);    2.//初始化物件
  • instance=memory;    3.//設定instance指向剛分配的記憶體


    上面2,3可能會被重排序,如果重排序後,當A執行了執行緒執行到了3(2還沒有執行),B執行緒獲取這個單例,判斷不為空(已經有指向記憶體空間了),但實際上並沒有初始化,這裡訪問的也就是沒有初始化的物件。如下圖兩個執行緒訪問順序:
    在這裡插入圖片描述
    解決辦法:
       1>: 禁止重排序 所以通過把instance用volatile申明,那麼當前就會禁止重排序就會避免這個問題
       2>: 通過類初始化完成,即餓漢式單例模式(類載入使這個重排序對外隱藏)

5.volatile使用條件

您只能在有限的一些情形下使用 volatile 變數替代鎖。要使 volatile 變數提供理想的執行緒安全,必須同時滿足下面兩個條件:

  • 對變數的寫操作不依賴於當前值,例如自增環境下不行 i++;
  • 該變數沒有包含在具有其他變數的不變式中

6.volatile應用場景

  1. volatile特別適合於狀態標記量
    例如: volatile boolean flag = true;
  2. 雙重檢查鎖定-單例模式使用,如上所示
  3. 其他場景可以參考:https://www.ibm.com/developerworks/cn/java/j-jtp06197.html