Java并發(fā)編程之volatile與JMM多線程內(nèi)存模型

一、通過程序看現(xiàn)象
在開始為大家講解Java 多線程緩存模型之前,我們先看下面的這一段代碼。這段代碼的邏輯很簡單:主線程啟動了兩個子線程,一個線程1、一個線程2。線程1先執(zhí)行,sleep睡眠2秒鐘之后線程2執(zhí)行。兩個線程使用到了一個共享變量shareFlag,初始值為false。如果shareFlag一直等于false,線程1將一直處于死循環(huán)狀態(tài),所以我們在線程2中將shareFlag設(shè)置為true。
public class VolatileTest {
public static boolean shareFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.print("開始執(zhí)行線程1 =>");
while (!shareFlag){ //shareFlag = false則一直死循環(huán)
//System.out.println("shareFlag=" + shareFlag);
}
System.out.print("線程1執(zhí)行完成 =>");
}).start();
Thread.sleep(2000);
new Thread(() -> {
System.out.print("開始執(zhí)行線程2 =>");
shareFlag = true;
System.out.print("線程2執(zhí)行完成 =>");
}).start();
}
}
如果你沒有學(xué)過JMM線程模型,可能你看完上面的代碼,希望得到的輸出結(jié)果是下面這樣的:
開始執(zhí)行線程1 =>開始執(zhí)行線程2 =>線程2執(zhí)行完成 =>線程1執(zhí)行完成=>
如下圖所示,正常人理解這段代碼,首先執(zhí)行線程1進入循環(huán),線程2修改shareFlag=true,線程1跳出循環(huán)。所以跳出循環(huán)的線程1會打印"線程1執(zhí)行完成=>",但是經(jīng)過筆者實驗,**"線程1執(zhí)行完成=>"不會被打印,線程1也沒有跳出死循環(huán)**,這是為什么呢?

二、為什么會產(chǎn)生這種現(xiàn)象(JMM模型)?
要解釋上面提到的問題,我們就需要學(xué)習(xí)JMM(Java Memory Model)Java 內(nèi)存模型,筆者覺得叫做Java多線程內(nèi)存模型更準(zhǔn)確一些。

- 首先,在JMM中每個線程有自己的工作內(nèi)存,在程序啟動的時候,線程將共享變量加載(read&load)到自己的工作內(nèi)存中,加載到線程工作內(nèi)存中的內(nèi)存變量是主內(nèi)存中共享變量的副本。也就是說此時shareFlag在內(nèi)存中有三個副本,值都等于false。
- 當(dāng)線程2執(zhí)行
shareFlag=true的時候?qū)⑵涔ぷ鲀?nèi)存副本修改為shareFlag=true,同時將副本的值同步寫回(store&write)到主內(nèi)存中。 - 但是線程1的工作內(nèi)存中的
shareFlag=false沒有發(fā)生變化,所以線程1一直處于死循環(huán)之中。
三、MESI 緩存一致性協(xié)議
按照上文的實驗以及JMM模型,線程2修改的共享變量的值,線程1感知不到。那怎么樣才能讓線程1感知到共享變量的值發(fā)生了變化呢?其實也很簡單,給shareFlag共享變量加上volatile關(guān)鍵字就可以了。
public volatile static boolean shareFlag = false;
其底層原理是這樣的,加上volatile關(guān)鍵字提示JMM遵循MESI 緩存一致性協(xié)議,該協(xié)議包含如下的緩存使用規(guī)范(看不懂可以不看,下文會用簡單的語言及例子描述一下)。
- Modified:代表當(dāng)前Cache行的數(shù)據(jù)是修改過的(Dirty),并且只在當(dāng)前CPU的Cache中是修改過的;此時該Cache行的數(shù)據(jù)與其他Cache中的數(shù)據(jù)不同,與內(nèi)存中該行的數(shù)據(jù)也不同。
- Exclusive:代表當(dāng)前Cache行的數(shù)據(jù)是有效數(shù)據(jù),其他CPU的Cache中沒有這行數(shù)據(jù);并且當(dāng)前Cache行數(shù)據(jù)與內(nèi)存中的數(shù)據(jù)相同。
- Shared:代表多個CPU的Cache中都會緩存有這行數(shù)據(jù),并且Cache中的數(shù)據(jù)與內(nèi)存中的數(shù)據(jù)一致;
- Invalid:表示當(dāng)前Cache行中的數(shù)據(jù)無效;

上文中的緩存使用規(guī)范可能過于復(fù)雜,簡單的說就是
- 當(dāng)線程2修改shareFlag的時候(參考Modify),告知bus總線我修改了共享變量shareFlag,
- 線程1對Bus總線進行監(jiān)聽,當(dāng)它獲知共享變量shareFlag發(fā)生了修改就會將自己工作內(nèi)存中的shareFlag副本刪除使其失效。
- 當(dāng)線程1再次需要使用到shareFlag的時候,發(fā)現(xiàn)工作內(nèi)存中沒有shareFlag變量副本,就會重新從主內(nèi)存中加載(read&load)
到此這篇關(guān)于Java并發(fā)-volatile與JMM多線程內(nèi)存模型的文章就介紹到這了,更多相關(guān)java多線程內(nèi)存模型內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java字節(jié)與字符流永久存儲json數(shù)據(jù)
本篇文章給大家詳細講述了Java字節(jié)與字符流永久存儲json數(shù)據(jù)的方法,以及代碼分享,有興趣的參考學(xué)習(xí)下。2018-02-02
Springboot靜態(tài)資源訪問實現(xiàn)代碼解析
這篇文章主要介紹了Springboot靜態(tài)資源訪問實現(xiàn)代碼解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-06-06
springboot集成開發(fā)實現(xiàn)商場秒殺功能
這篇文章主要介紹了springboot集成實現(xiàn)商品秒殺功能,秒殺系統(tǒng)業(yè)務(wù)流程,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-12-12
MyBatis批量更新(update foreach)報錯問題

