在開始跟大家講解Java 多執行緒快取模型之前,我們先看下面的這段程式碼。這段程式碼的邏輯很簡單:主執行緒啟動了兩個子線程,一個線程1、一個線程2。執行緒1先執行,sleep睡眠2秒鐘之後執行緒2執行。兩個執行緒使用到了一個共享變數shareFlag,初始值為false。 如果shareFlag一直等於false,執行緒1將一直處於死循環狀態,所以我們在執行緒2中將shareFlag設為true。
public class VolatileTest { public static boolean shareFlag = false; public static void main(String[] args) throws InterruptedException { new Thread(() -> { System.out.print("开始执行线程1 =>"); while (!shareFlag){ //shareFlag = false则一直死循环 //System.out.println("shareFlag=" + shareFlag); } System.out.print("线程1执行完成 =>"); }).start(); Thread.sleep(2000); new Thread(() -> { System.out.print("开始执行线程2 =>"); shareFlag = true; System.out.print("线程2执行完成 =>"); }).start(); } }
如果你沒有學過JMM執行緒模型,可能你看完上面的程式碼,希望得到的輸出結果是下面這樣的:
開始執行執行緒1 =>開始執行線程2 =>線程2執行完成=>線程1執行完成=>
如下圖所示,正常人理解這段程式碼,首先執行線程1進入循環,線程2修改shareFlag=true,線程1跳出循環。所以跳出循環的線程1會打印"線程1執行完成=>",但是經過筆者實驗,**"線程1執行完成=>"不會被打印,線程1也沒有跳出死循環**,這是為什麼呢?
要解釋上面提到的問題,我們就需要學習JMM(Java Memory Model)Java 記憶體模型,筆者覺得叫做Java多執行緒記憶體模型比較準確一些。
首先,在JMM中每個執行緒有自己的工作內存,在程式啟動的時候,執行緒將共享變數載入(read&load)到在自己的工作記憶體中,載入到執行緒工作記憶體中的記憶體變數是主記憶體中共享變數的副本。也就是說此時shareFlag在記憶體中有三個副本,數值都等於false。
當執行緒2執行shareFlag=true
的時候將其工作記憶體副本修改為shareFlag=true
,同時將副本的值同步寫回(store&write)到主記憶體。
但是線程1的工作記憶體中的shareFlag=false
沒有發生變化,所以線程1一直處於死循環之中。
線程2對共享變數的修改不會被執行緒1感知,這符合上文的實驗結果和JMM模型。那怎麼樣才能讓線程1感知到共享變數的值改變了呢?其實也很簡單,給shareFlag共享變數加上volatile關鍵字就可以了。
public volatile static boolean shareFlag = false;
其底層原理是這樣的,加上volatile關鍵字提示JMM遵循MESI 快取一致性協議,該協議包含如下的快取使用規範(看不懂可以不看,下文會用簡單的語言及例子描述一下)。
Modified:代表目前Cache行的資料是已修改的(Dirty),並且只在目前CPU的Cache中是修改過的;此時該Cache行的資料與其他Cache中的資料不同,與記憶體中該行的資料也不同。
Exclusive:代表當前Cache行的數據是有效數據,其他CPU的Cache中沒有這行數據;並且當前Cache行數據與記憶體中的數據相同。
Shared:代表多個CPU的Cache中都會快取有這行數據,且Cache中的資料與記憶體中的資料一致;
Invalid:表示目前Cache行中的資料無效;
上文中的快取使用規格可能過於複雜,簡單的說就是
當執行緒2修改shareFlag的時候(參考Modify),告知bus匯流排我修改了共享變數shareFlag,
#線程1對Bus匯流排進行監聽,當它獲知共享變數shareFlag發生了修改就會將自己工作記憶體中的shareFlag副本刪除使其失效。
當執行緒1再次需要使用到shareFlag的時候,發現工作記憶體中沒有shareFlag變數副本,就會重新從主記憶體載入(read&load)
以上是Java並發程式設計之volatile與JMM多執行緒記憶體模型實例分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!