1. 程式人生 > 程式設計 >java多執行緒volatile記憶體語義解析

java多執行緒volatile記憶體語義解析

這篇文章主要介紹了java多執行緒volatile記憶體語義解析,文中通過示例程式碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下

  volatile關鍵字是java虛擬機器提供的最輕量級額的同步機制。由於volatile關鍵字與java記憶體模型相關,因此,我們在介紹volatile關鍵字之前,對java記憶體模型進行更多的補充(之前的博文也曾介紹過)。

  1. java記憶體模型(JMM)

  JMM是一種規範,主要用於定義共享變數的訪問規則,目的是解決多個執行緒本地記憶體與共享記憶體的資料不一致、編譯器處理器的指令重排序造成的各種執行緒安全問題,以保障多執行緒程式設計的原子性、可見性和有序性。

  JMM規定了所有的變數都儲存在主記憶體中,每條執行緒還有自己的工作記憶體,執行緒中的工作記憶體中儲存了該執行緒用到的變數的主記憶體的拷貝,各執行緒對變數的所有操作都必須在工作記憶體中進行,
執行緒之間的變數值的傳遞都必須通過主記憶體來進行。

  JMM定義了8中操作實現主記憶體與工作記憶體的互動協議:

  •     1)lock:作用於主記憶體,它把一個變數標識為一條執行緒的獨佔狀態。
  •     2)unlock:作用於主記憶體,它把一個處於鎖定狀態的變數的釋放出來。
  •     3)read:作用於主記憶體,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中。
  •     4)load:作用於工作記憶體,它把從主記憶體中read到的值放入工作記憶體的變數副本中。
  •     5)use:作用於工作記憶體,它把一個變數的值從主記憶體傳遞給執行引擎
  •     6)assign:作用與工作記憶體,它把一個從執行引擎接收到的值賦值給工作記憶體的變數。
  •     7)store:作用於工作記憶體,把工作記憶體中一個變數的值傳送到主記憶體。
  •     8)write:作用於主記憶體,它把store操作從工作記憶體中得到的值放入主記憶體中的變數中。

  這8中操作以及對著8中操作的規則的限制就能確定哪些記憶體訪問在併發條件下是執行緒安全的,這種方式比較繁瑣,jdk1.5之後提出了提出了happens-before規則來判斷執行緒是否安全。

  可以這麼理解,happens-before規則是JMM的核心.Happens-before就是用來確定兩個操作的執行順序。這兩個操作可在同一執行緒中,也可以在兩個執行緒中。

happens-before規定:如果一個操作happens-before另個一操作,那麼第一個操作的結果對第二個操作可見(但這並不意味著處理器必須按照happens-before順序執行,只要不改變執行結果,可任意優化)。happens-before規則已在前邊博文中介紹,這裡不再重複(http://www.cnblogs.com/gdy1993/p/9117331.html)

  JMM記憶體規則僅僅是一種規則,規則的最終落實是通過java虛擬機器、編譯器以及處理器一同協作來落實的,而記憶體屏障是java虛擬機器、編譯器、處理器之間溝通的紐帶。

而java原因封裝了這些底層的具體實現與控制,提供了synchronized、lock和volatile等關鍵字的來保障多執行緒安全問題。

  2. volatile關鍵字

  (1)volatile對可見性的保證

  在介紹volatile關鍵字之前,先來看這樣一段程式碼:

//執行緒1
    boolean stop = false;
    while(!stop) {
      doSomething();
    }
    //執行緒2
    stop = true;

  有兩個執行緒:執行緒1和執行緒2,執行緒1在stop==false時,不停的執行doSomething()方法;執行緒2在執行到一定情況時,將stop設定為true,將執行緒1中斷,很多人採用這種方式中斷執行緒,但這並不是安全的。因為stop作為一個普通變數,執行緒2對其的修改,並不能立刻被執行緒1所感知,即執行緒1對stop的修改僅僅在自己的工作記憶體中,還沒來的急寫入主記憶體,執行緒2工作記憶體中的stop並未修改,可能導致執行緒無法中斷,雖然這種可能性很小,但一旦發生,後果嚴重。

  而使用volatile變數修飾就能避免這個問題,這也是volatile第一個重要含義:

    volatile修飾的變數,能夠保證不同執行緒對這個變數操作的可見性,即一個執行緒修改了這個變數的值,這個新值對於其他執行緒是立即可見的。

    volatile的對可見性保證的原理:

  對於volatile修飾的變數,當某個執行緒對其進行修改時,會強制將該值重新整理到主記憶體,這就使得其他執行緒對該變數在各自工作記憶體中的快取無效,因而在其他執行緒對該變數進行操作時,必須從主記憶體中重新載入

   (2)volatile對原子性的保障?

  首先來看這樣一段程式碼(深入理解java虛擬機器):

public class VolatileTest {
  public static volatile int race = 0;
  
  public static void increase() {
    race++;
  }
  
  public static final int THREAD_COUNT = 20;
  
  public static void main(String[] args) {
    Thread[] threads = new Thread[THREAD_COUNT];
    for (Thread t : threads) {
      t = new Thread(new Runnable() {
        
        @Override
        public void run() {
          for(int i = 0; i < 10000; i++) {
            increase();
          }
        }
      });
      t.start();
    }
    
    while(Thread.activeCount() > 1) {
      Thread.yield();
    }
    
    System.out.println(race);//race < 200000
    
  }
}

  race是volatile修飾的共享變數,建立20個執行緒對這個共享變數進行自增操作,每個執行緒自增的次數為10000次,如果volatile能夠保證原子性的話,最終race的結果肯定是200000。但結果不然,每次程式執行race'的值總是小於200000,這也側面證明了volatile並不能保證共享變數操作的原子性。原理如下:

  執行緒1讀取了race的值,然後cp分配的時間片結束,執行緒2此時讀取了共享變數的值,並對race進行自增操作,並將操作後的值重新整理到主記憶體,此時執行緒1已經讀取了race的值,因此保留的依然是原來的值,此時這個值已是舊值,對race進行自增操作後重新整理到主記憶體,因此主記憶體中的值也是舊值。這也是volatile僅僅能保障讀到的是相對新值的原因。

  (3)volatile對有序性的保障

  首先來看這樣一段程式碼:

//執行緒1
    boolean initialized = false;
    context = loadContext();
    initialized = true;
    //執行緒2
    while(!initialized) {
      sleep();
    }
    doSomething(context);

  執行緒2在initialized變數為true時,使用context變數完成一些操作;執行緒1負責載入context,並在載入完成後將initialized變數設為true。但是,由於initialized只是一個普通變數,普通變數僅僅能夠保證在該方法的執行過程中,所有依賴賦值結果的地方都能獲得正確的值,而不能保證變數的賦值順序與程式程式碼的執行順序一致。因此就可能出現這樣一種情況,當執行緒1將initialized變數設為true時,context依然沒有載入完成,但執行緒2由於讀到initialized為true,就可能執行了doSomething()方法,可能會產生非常奇怪的效果。

  而volatile的第二個語義就是禁止重排序: 

    寫volatile變數的操作與該操作之前的任何讀寫操作都不會被重排序;

    讀volatile變數操作與該操作之後的任何讀寫操作都不會重排序。

  (4) volatile的底層實現原理

  java語言底層是通過記憶體屏障來實現volatile語義的。

  對於volatile變數的寫操作:

  ①java虛擬機器會在該操作之前插入一個釋放屏障(loadstore+storestore),釋放屏障禁止了volatile變數的寫操作與該操作之前的任何讀寫操作的重排序。

  ②java虛擬機器會在該操作之後插入一個儲存屏障(storeload),儲存屏障使得對volatile變數的寫操作能夠同步到主記憶體。

  對於volatile變數的讀操作:

  ③java虛擬機器會在該操作之前插入一個loadload,使得每次對volatile變數的讀取都從主記憶體中重新載入(重新整理處理器快取)

  ④java虛擬機器會在該操作之後插入一個獲得屏障(loadstore+loadload),使得volatile後的任何讀寫操作與該操作進行重排序。

  ①③保障可見性,②④保障有序性。

  (5)volatile關鍵字與happens-before的關係

  Happens-before規則中的volatile規則為:對於一個volatile域的寫happens-before後續每一個針對該變數的讀操作。

  寫執行緒執行write(),然後讀執行緒執行read()方法,圖中每個箭頭都代表一個happens-before關係,黑色箭頭是根據程式順序規則,藍色箭頭根據volatile規則,紅色箭頭是根據傳遞性推出的,即操作2happens-before操作3,即對volatile共享變數的更新操作排在後續讀取操作之前,對volatile變數的修改對後續volatile變數的讀取可見。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。