java高并發(fā)的volatile與Java內(nèi)存模型詳解
public class Demo09 { public static boolean flag = true; public static class T1 extends Thread { public T1(String name) { super(name); } @Override public void run() { System.out.println("線程" + this.getName() + " in"); while (flag) { ; } System.out.println("線程" + this.getName() + "停止了"); } } public static void main(String[] args) throws InterruptedException { new T1("t1").start(); //休眠1秒 Thread.sleep(1000); //將flag置為false flag = false; } }
運(yùn)行上面代碼,會(huì)發(fā)現(xiàn)程序無(wú)法終止。
線程t1的run()方法中有個(gè)循環(huán),通過(guò)flag來(lái)控制循環(huán)是否結(jié)束,主線程中休眠了1秒,將flag置為false,按說(shuō)此時(shí)線程t1會(huì)檢測(cè)到flag為false,打印“線程t1停止了”,為何和我們期望的結(jié)果不一樣呢?運(yùn)行上面的代碼我們可以判斷,t1中看到的flag一直為true,主線程將flag置為false之后,t1線程中并沒(méi)有看到,所以一直死循環(huán)。
那么t1中為什么看不到被主線程修改之后的flag?
要解釋這個(gè),我們需要先了解一下java內(nèi)存模型(JMM),Java線程之間的通信由Java內(nèi)存模型(本文簡(jiǎn)稱(chēng)為JMM)控制,JMM決定一個(gè)線程對(duì)共享變量的寫(xiě)入何時(shí)對(duì)另一個(gè)線程可見(jiàn)。從抽象的角度來(lái)看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存(main memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(local memory),本地內(nèi)存中存儲(chǔ)了該線程以讀/寫(xiě)共享變量的副本。本地內(nèi)存是JMM的一個(gè)抽象概念,并不真實(shí)存在。它涵蓋了緩存,寫(xiě)緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化。Java內(nèi)存模型的抽象示意圖如下:
從上圖中可以看出,線程A需要和線程B通信,必須要經(jīng)歷下面2個(gè)步驟:
1.首先,線程A把本地內(nèi)存A中更新過(guò)的共享變量刷新到主內(nèi)存中去
2.然后,線程B到主內(nèi)存中去讀取線程A之前已更新過(guò)的共享變量
下面通過(guò)示意圖來(lái)說(shuō)明這兩個(gè)步驟:
如上圖所示,本地內(nèi)存A和B有主內(nèi)存中共享變量x的副本。假設(shè)初始時(shí),這三個(gè)內(nèi)存中的x值都為0。線程A在執(zhí)行時(shí),把更新后的x值(假設(shè)值為1)臨時(shí)存放在自己的本地內(nèi)存A中。當(dāng)線程A和線程B需要通信時(shí),線程A首先會(huì)把自己本地內(nèi)存中修改后的x值刷新到主內(nèi)存中,此時(shí)主內(nèi)存中的x值變?yōu)榱?。隨后,線程B到主內(nèi)存中去讀取線程A更新后的x值,此時(shí)線程B的本地內(nèi)存的x值也變?yōu)榱?。從整體來(lái)看,這兩個(gè)步驟實(shí)質(zhì)上是線程A在向線程B發(fā)送消息,而且這個(gè)通信過(guò)程必須要經(jīng)過(guò)主內(nèi)存。JMM通過(guò)控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互,來(lái)為java程序員提供內(nèi)存可見(jiàn)性保證。
對(duì)JMM了解之后,我們?cè)倏纯次恼麻_(kāi)頭的問(wèn)題,線程t1中為何看不到被主線程修改為false的flag的值,有兩種可能:
1.主線程修改了flag之后,未將其刷新到主內(nèi)存,所以t1看不到
2.主線程將flag刷新到了主內(nèi)存,但是t1一直讀取的是自己工作內(nèi)存中flag的值,沒(méi)有去主內(nèi)存中獲取flag最新的值
對(duì)于上面2種情況,有沒(méi)有什么辦法可以解決?
是否有這樣的方法:線程中修改了工作內(nèi)存中的副本之后,立即將其刷新到主內(nèi)存;工作內(nèi)存中每次讀取共享變量時(shí),都去主內(nèi)存中重新讀取,然后拷貝到工作內(nèi)存。
java幫我們提供了這樣的方法,使用volatile修飾共享變量,就可以達(dá)到上面的效果,被volatile修改的變量有以下特點(diǎn):
1.線程中讀取的時(shí)候,每次讀取都會(huì)去主內(nèi)存中讀取共享變量最新的值,然后將其復(fù)制到工作內(nèi)存
2.線程中修改了工作內(nèi)存中變量的副本,修改之后會(huì)立即刷新到主內(nèi)存
我們修改一下開(kāi)頭的示例代碼:
public volatile static boolean flag = true;
使用volatile修飾flag變量,然后運(yùn)行一下程序,輸出:
線程t1 in 線程t1停止了
這下程序可以正常停止了。volatile解決了共享變量在多線程中可見(jiàn)性的問(wèn)題,可見(jiàn)性是指一個(gè)線程對(duì)共享變量的修改,對(duì)于另一個(gè)線程來(lái)說(shuō)是否是可以看到的。\
總結(jié)
本篇文章就到這里了,希望能夠給你帶來(lái)幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
JDK8接口的默認(rèn)與靜態(tài)方法-接口與抽象類(lèi)的區(qū)別詳解
這篇文章主要介紹了JDK8接口的默認(rèn)與靜態(tài)方法-接口與抽象類(lèi)的區(qū)別詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,,需要的朋友可以參考下2019-06-06springboot整合token的實(shí)現(xiàn)代碼
這篇文章主要介紹了springboot整合token的實(shí)現(xiàn)代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11Spring的連接數(shù)據(jù)庫(kù)以及JDBC模板(實(shí)例講解)
下面小編就為大家?guī)?lái)一篇Spring的連接數(shù)據(jù)庫(kù)以及JDBC模板(實(shí)例講解)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10java配置dbcp連接池(數(shù)據(jù)庫(kù)連接池)示例分享
java配置dbcp連接池示例分享,大家參考使用吧2013-12-12SpringBoot搭建多數(shù)據(jù)源的實(shí)現(xiàn)方法
說(shuō)起多數(shù)據(jù)源,一般都來(lái)解決那些問(wèn)題呢,主從模式或者業(yè)務(wù)比較復(fù)雜需要連接不同的分庫(kù)來(lái)支持業(yè)務(wù)。本文主要介紹了SpringBoot搭建多數(shù)據(jù)源的實(shí)現(xiàn)方法,感興趣的可以了解一下,感興趣的可以額了解一下2021-12-12springboot 中整合mybatis多數(shù)據(jù)源不使用JPA
這篇文章主要介紹了springboot 中整合mybatis多數(shù)據(jù)源不使用JPA,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08SpringBoot 攔截器和自定義注解判斷請(qǐng)求是否合法
這篇文章主要介紹了SpringBoot 攔截器和自定義注解判斷請(qǐng)求是否合法,幫助大家更好的理解和使用springboot框架,感興趣的朋友可以了解下2020-12-12