我們之前講解了JMM模型,以及其引入的必要行,以及JMM與JVM內存模型的比較和JMM與硬件內存結構的對應關系。
思維導圖
本節主要講解思維導圖如下:

內容
1、JMM的8大原子操作
1、lock(鎖定):作用於主內存的變量,它把一個變量標識為一條線程獨占的狀態。
2、unlock(解鎖):作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放后的變量 才可以被其他線程鎖定。
3、read(讀取):作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以 便隨后的load動作使用。
4、load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的 變量副本中。
5、use(使用):作用於工作內存的變量,它把工作內存中一個變量的值傳遞給執行引擎,每當虛 擬機遇到一個需要使用變量的值的字節碼指令時將會執行這個操作。
6、assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收的值賦給工作內存的變量, 每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
7、store(存儲):作用於工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨 后的write操作使用。
8、write(寫入):作用於主內存的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中。
注意:
1、如果需要把變量總主內存賦值給工作內存:read和load必須是連續;read只是把主內存的變量值從主內存加載到工作內存中,而load是真正把工作內存的值放到工作內存的變量副本中。
2、如果需要把變量從工作內存同步回主內存;就需要執行順序執行store跟write操作。store作用於工作內存,將工作內存變量值加載到主內存中,write是將主內存里面的值放入主內存的變量中。
代碼實例:
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(); } }
我們發現上面代碼數據結果為:
線程: thread1 修改共享變量flag為true
並且主線程不會退出,說明有用戶線程在runnable運行中,說明線程2一直在運行,也說明線程2獲取的變量值先從主內存read到工作內存,然后load給線程2里面工作內存里面變量,然后線程2一直是從自己工作內存獲取數據,並且線程2是while的空轉,搶占cpu時間多,所以一直不退出。
2、基於8大原子操作程序數據加載回寫流程
8大原子操作是怎樣做的?變量是如何讀取、如何賦值的?

上面是線程2執行后的結果;所以線程2先讀取到flag=false;所以先不會退出。
接着線程1會執行修改flag的操作。將flag修改成true;
第1步:read變量到
第2步: load到工作內存里去;
第3步: use傳遞給執行引擎做賦值操作。
第4步: 將修改后的值assign到工作內存;這個值會從false變成true;
那么工作內存里面的新值flag=true會立馬同步到主內存里面去嗎?
更新后的新值不會立馬同步到我們的主內存里面去,他需要等待一定的時機。時機到了之后會同步到我們的主內存中去;
同步的時候也需要分為執行兩步驟:store和write操作。
但是更新到主內存為true之后,為什么我們的線程2為什么沒有感知到了;原因線程2在while進行循環判斷的時候,一直判斷的是我們線程2自己的工作內存里面的值。執行引擎一直判斷;判斷的值一直是工作內存里面的值。
然后我們修改代碼如下;在while循環判斷里面加一個i++的話,那么我們的線程2能不能及時感知到flag變化的值呢?
因為工作內存中已經存在這個值的話,就不會從主內存去加載。
我們修改代碼如下:線程3去讀取主內存flag的值,因為線程3是從主內存加載的線程1已經寫入的值,此時這個值是flag=true;所以ok。
然后我們加上一個同步代碼快之后的效果呢?
通過上面分析,我們的線程2已經感知到了flag數據的變化。 這是什么原因呢?這里很多人都搞不明白,這里有一個很大的坑:加了同步快之后,我們的線程2就能夠讀取到我們線程1修改的數據,這個是為什么呢?
原因:之前我們說了,之前沒有加同步代碼塊之前,我們程序指令一直在循環/或者一直在做i++操作。循環是空的,可以理解為其近似在自旋跑;此時此線程對cpu的使用權限是特別高的;別的線程壓根就搶不到cpu的時間片。我們加了同步快之后,我們此時線程會產生阻塞(cpu的使用權限被別的線程搶去了)。產生阻塞之后會發生線程上下文切換。如下:

2、可見性
可見性: 一個線程對某個共享主內存變量進行修改之后,其他與此共享變量相關的線程會立馬感知到這個數據的更改。其他線程可以看到某個線程修改后的值。
之前代碼我們發現,我們兩個線程一個線程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

緩存一致性原理再次剖析:
線程1跟線程2都已經將flag=false的值加載到各自的工作內存,此時flag的狀態都是S狀態(共享狀態),此時線程2將修改flag的值為true時候,其狀態變成了M狀態,這個時候線程1所在的cpu會嗅探到flag值修改讓后將flag對應的緩存行狀態設置為I(無效狀態),然后我們線程1需要使用的時候由於值無效,需要重新加載,此時需要重新加載的話,需要線程2將修改的值添加到主內存,然后線程1才能夠加載到正確的值。
Java內存模型內存交互操作:
把一個變量從主內存中復制到工作內存中,就需要按順序地執行read個load操作,如果把變量從工作內存中同步到主內存中,就需要按照順序地執行 store個write操作。但是Java內存模型只要求上述操作必須按照順序執行,而沒有保證必須是連續執行的。

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