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進(jìn)入循環(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總線進(jìn)行監(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)文章
Spring實戰(zhàn)之使用注解實現(xiàn)聲明式事務(wù)操作示例
這篇文章主要介紹了Spring實戰(zhàn)之使用注解實現(xiàn)聲明式事務(wù)操作,結(jié)合實例形式詳細(xì)分析了spring使用注解實現(xiàn)聲明式事務(wù)相關(guān)配置、接口實現(xiàn)與使用技巧,需要的朋友可以參考下2020-01-01Java Spring動態(tài)生成Mysql存儲過程詳解
這篇文章主要介紹了Java Spring動態(tài)生成Mysql存儲過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-06-06使用springboot整合websocket實現(xiàn)群聊教程
websocket怎么說呢,就是服務(wù)器可以主動向客戶端發(fā)起對話,下面就是springboot整合websocket實現(xiàn)群聊的操作代碼,一起來看一下get新技能吧2021-08-08Mybatis中的config.xml配置文件詳細(xì)解析
這篇文章主要介紹了詳解Mybatis-config.xml配置文件,需要的朋友可以參考下2017-12-12Java中的靜態(tài)綁定和動態(tài)綁定詳細(xì)介紹
這篇文章主要介紹了Java中的靜態(tài)綁定和動態(tài)綁定詳細(xì)介紹,在Java中存在兩種綁定方式,一種為靜態(tài)綁定,又稱作早期綁定,另一種就是動態(tài)綁定,亦稱為后期綁定,需要的朋友可以參考下2015-01-01