Java併發-JMM的8大原子操做及併發3之volatile關鍵字可見性

摘要

咱們以前講解了JMM模型,以及其引入的必要行,以及JMM與JVM內存模型的比較和JMM與硬件內存結構的對應關係。segmentfault

思惟導圖

本節主要講解思惟導圖以下: 緩存

image.png

內容

一、JMM的8大原子操做

一、lock(鎖定):做用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態。
二、unlock(解鎖):做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量 才能夠被其餘線程鎖定。
三、read(讀取):做用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以 便隨後的load動做使用。
四、load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的 變量副本中。
五、use(使用):做用於工做內存的變量,它把工做內存中一個變量的值傳遞給執行引擎,每當虛 擬機遇到一個須要使用變量的值的字節碼指令時將會執行這個操做。
六、assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收的值賦給工做內存的變量, 每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
七、store(存儲):做用於工做內存的變量,它把工做內存中一個變量的值傳送到主內存中,以便隨 後的write操做使用。
八、write(寫入):做用於主內存的變量,它把store操做從工做內存中獲得的變量的值放入主內存的變量中。多線程

注意:
一、若是須要把變量總主內存賦值給工做內存:read和load必須是連續;read只是把主內存的變量值從主內存加載到工做內存中,而load是真正把工做內存的值放到工做內存的變量副本中。
二、若是須要把變量從工做內存同步回主內存;就須要執行順序執行store跟write操做。store做用於工做內存,將工做內存變量值加載到主內存中,write是將主內存裏面的值放入主內存的變量中。 優化

代碼實例:this

public class VolatileTest2 {
     static boolean  flag = false;

     public void refresh(){
         this.flag = true;
         String threadName = Thread.currentThread().getName();
         System.out.println("線程: "+threadName+" 修改共享變量flag爲"+flag);
     }
     public void load(){
         String threadName = Thread.currentThread().getName();
         while (!flag){

         }
         System.out.println("線程: "+threadName+" 嗅探到flag狀態的改變"+" flag:"+flag);
     }
     public static void main(String[] args) {
         /**
          * 建立兩個線程
          */
         VolatileTest2 obj = new VolatileTest2();
         Thread thread1 = new Thread(() -> {
             obj.refresh();
         }, "thread1");
         Thread thread2 = new Thread(() -> {
             obj.load();
         }, "thread2");

         thread2.start();
         try {
             /**
              * 確保咱們線程2先執行
              */
              Thread.sleep(2000);
         }catch (Exception e){
             e.printStackTrace();
         }
         thread1.start();
     }
}

咱們發現上面代碼數據結果爲:spa

線程: thread1 修改共享變量flag爲true

而且主線程不會退出,說明有用戶線程在runnable運行中,說明線程2一直在運行,也說明線程2獲取的變量值先從主內存read到工做內存,而後load給線程2裏面工做內存裏面變量,而後線程2一直是從本身工做內存獲取數據,而且線程2是while的空轉,搶佔cpu時間多,因此一直不退出。線程

二、基於8大原子操做程序數據加載回寫流程

8大原子操做是怎樣作的?變量是如何讀取、如何賦值的?3d

image.png

上面是線程2執行後的結果;因此線程2先讀取到flag=false;因此先不會退出。 code

接着線程1會執行修改flag的操做。將flag修改爲true;
第1步:read變量到
第2步: load到工做內存裏去;
第3步: use傳遞給執行引擎作賦值操做。
第4步: 將修改後的值assign到工做內存;這個值會從false變成true;blog

那麼工做內存裏面的新值flag=true會立馬同步到主內存裏面去嗎?
更新後的新值不會立馬同步到咱們的主內存裏面去,他須要等待必定的時機。時機到了以後會同步到咱們的主內存中去;

同步的時候也須要分爲執行兩步驟:store和write操做。
可是更新到主內存爲true以後,爲何咱們的線程2爲何沒有感知到了;緣由線程2在while進行循環判斷的時候,一直判斷的是咱們線程2本身的工做內存裏面的值。執行引擎一直判斷;判斷的值一直是工做內存裏面的值。

而後咱們修改代碼以下;在while循環判斷裏面加一個i++的話,那麼咱們的線程2能不能及時感知到flag變化的值呢?
image.png
由於工做內存中已經存在這個值的話,就不會從主內存去加載。

咱們修改代碼以下:線程3去讀取主內存flag的值,由於線程3是從主內存加載的線程1已經寫入的值,此時這個值是flag=true;因此ok。
image.png

而後咱們加上一個同步代碼快以後的效果呢?
image.png

經過上面分析,咱們的線程2已經感知到了flag數據的變化。 這是什麼緣由呢?這裏不少人都搞不明白,這裏有一個很大的坑:加了同步快以後,咱們的線程2就可以讀取到咱們線程1修改的數據,這個是爲何呢?

緣由:以前咱們說了,以前沒有加同步代碼塊以前,咱們程序指令一直在循環/或者一直在作i++操做。循環是空的,能夠理解爲其近似在自旋跑;此時此線程對cpu的使用權限是特別高的;別的線程壓根就搶不到cpu的時間片。咱們加了同步快以後,咱們此時線程會產生阻塞(cpu的使用權限被別的線程搶去了)。產生阻塞以後會發生線程上下文切換。以下:

image.png

二、可見性

可見性: 一個線程對某個共享主內存變量進行修改以後,其餘與此共享變量相關的線程會立馬感知到這個數據的更改。其餘線程能夠看到某個線程修改後的值。
以前代碼咱們發現,咱們兩個線程一個線程1修改掉flag的值以後,線程2是load讀取不到寫的值的,那麼爲了保證線程將簡單標記爲變量的可見性。咱們最簡單的方式是使用volatile關鍵字進行修改這個多線程共享的變量。

public class VolatileTest2 {
     static volatile boolean  flag = false;
     public void refresh(){
         this.flag = true;
         String threadName = Thread.currentThread().getName();
         System.out.println("線程: "+threadName+" 修改共享變量flag爲"+flag);
     }
     public void load(){
         String threadName = Thread.currentThread().getName();
         while (!flag){

         }
         System.out.println("線程: "+threadName+" 嗅探到flag狀態的改變"+" flag:"+flag);
     }
     public static void main(String[] args) {
         /**
          * 建立兩個線程
          */
         VolatileTest2 obj = new VolatileTest2();
         Thread thread1 = new Thread(() -> {
             obj.refresh();
         }, "thread1");
         Thread thread2 = new Thread(() -> {
             obj.load();
         }, "thread2");

         thread2.start();
         try {
             /**
              * 確保咱們線程2先執行
              */
              Thread.sleep(2000);
         }catch (Exception e){
             e.printStackTrace();
         }
         thread1.start();
     }
}

輸出結果以下:

線程: thread1 修改共享變量flag爲true
線程: thread2 嗅探到flag狀態的改變 flag:true

volatile底層原理
volatile是Java虛擬機提供的輕量級的同步機制
volatile語義有以下兩個做用:

  • 可見性:保證被volatile修飾的共享變量對全部線程老是可見的,也就是當一個線程修改了被volatile修飾的共享變量的值,新值老是能夠被其餘線程當即得知。
  • 有序性:禁止指令重排序優化:內存屏障。

volatile緩存可見性實現原理:

  • JMM內存交互層面:volatile修飾的變量的read、load、use操做和assign、store、write必須是連續的,即修改後必須當即同步到主內存,使用時必須從主內存刷新,由此保證volatile可見性。
  • 底層實現:經過彙編lock前綴指令,他會鎖定變量緩存行區域並寫會主內存,這個操做成爲「緩存鎖定」,緩存一致性機制會阻止同時修改兩個以上處理器緩存的內存區域數據。一個處理器的緩存回寫到內存會致使其餘處理器緩存失效。

彙編代碼查看:

  • -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp

image.png

緩存一致性原理再次剖析:
線程1跟線程2都已經將flag=false的值加載到各自的工做內存,此時flag的狀態都是S狀態(共享狀態),此時線程2將修改flag的值爲true時候,其狀態變成了M狀態,這個時候線程1所在的cpu會嗅探到flag值修改讓後將flag對應的緩存行狀態設置爲I(無效狀態),而後咱們線程1須要使用的時候因爲值無效,須要從新加載,此時須要從新加載的話,須要線程2將修改的值添加到主內存,而後線程1纔可以加載到正確的值。

Java內存模型內存交互操做:
把一個變量從主內存中複製到工做內存中,就須要按順序地執行read個load操做,若是把變量從工做內存中同步到主內存中,就須要按照順序地執行 store個write操做。可是Java內存模型只要求上述操做必須按照順序執行,而沒有保證必須是連續執行的。

image.png

以上是順序性而不是連貫的,注意read跟load必須成對出現;store跟write必須成對出現。

相關文章
相關標籤/搜索