[Java多線程編程之八] Java內(nèi)存模型

一符糊、Java內(nèi)存模型 == JVM內(nèi)存模型扒袖?

??很多人都會(huì)認(rèn)為Java內(nèi)存模型就是JVM內(nèi)存模型馆类,但實(shí)際上是錯(cuò)的,Java內(nèi)存模型是一個(gè)抽象的概念嘴办,描述了Java語言的一組規(guī)則和規(guī)范瞬场,JVM實(shí)際上也不僅僅支持運(yùn)行Java代碼,還支持很多能在JVM上運(yùn)行的語言如JRuby涧郊、Scale等贯被,這是因?yàn)镴Ruby、Scale也有自己的語言規(guī)范妆艘,只要編譯出來的字節(jié)碼符合《Java虛擬機(jī)規(guī)范》彤灶,就可以在JVM上運(yùn)行。



??JVM不關(guān)心代碼是用哪種編程語言寫的双仍,只要編譯出來的指令碼符合JVM規(guī)范,那么就可以在JVM上運(yùn)行桌吃,所有語言在JVM上的內(nèi)存的結(jié)構(gòu)都是一樣的朱沃,JVM上的內(nèi)存模型圖如下。

??在JVM中茅诱,所有實(shí)例域逗物、靜態(tài)域和數(shù)組元素存儲(chǔ)在堆內(nèi)存中,堆內(nèi)存在線程之間共享瑟俭。局部變量翎卓、方法定義參數(shù)和異常處理器參數(shù)不會(huì)在線程之間共享,它們不會(huì)有內(nèi)存可見性問題摆寄,也不會(huì)收內(nèi)存模型的影響失暴。


??《Java語言規(guī)范》中對(duì)Java內(nèi)存模型的描述坯门,主要是針對(duì)多線程程序的語義,包括當(dāng)多個(gè)線程修改了共享內(nèi)存中的值時(shí)逗扒,應(yīng)該讀取到哪個(gè)值的規(guī)則古戴,這些語義沒有規(guī)定如何執(zhí)行多線程程序,相反它們描述了允許多線程程序的合法行為矩肩;所謂的“合法”现恼,其實(shí)就是保證多線程對(duì)共享數(shù)據(jù)訪問的可見性和修改的安全性。




二黍檩、Java內(nèi)存模型基礎(chǔ)

??在并發(fā)編程中叉袍,需要處理的兩個(gè)關(guān)鍵問題是:線程之前如何通信線程之間如何同步

1刽酱、通信

??通信是指線程之間以何種機(jī)制來交換信息喳逛。在命令式編程中,線程之間的通信機(jī)制有兩種:共享內(nèi)存消息傳遞肛跌。

??在共享內(nèi)存的并發(fā)模型里艺配,線程之間共享程序的公共狀態(tài),線程之間通過寫-讀內(nèi)存中的公共狀態(tài)隱式進(jìn)行通信衍慎。
??在消息傳遞的并發(fā)模型里转唉,線程之間沒有公共狀態(tài),線程之間必須通過明確的發(fā)送消息顯式進(jìn)行通信稳捆。


2赠法、同步

??同步是指程序用于控制不同線程之間操作發(fā)生相對(duì)順序的機(jī)制。在共享內(nèi)存的并發(fā)模型里乔夯,同步是顯式進(jìn)行的砖织,程序員必須顯式指定某個(gè)方法或某段代碼需要在線程之間互斥執(zhí)行;在消息傳遞的并發(fā)模型里末荐,由于消息的發(fā)送必須在消息的接收之前,因此同步是隱式進(jìn)行的。

??Java的并發(fā)采用的是共享內(nèi)存模型,Java線程之間的通信總是隱式進(jìn)行哲鸳,整個(gè)通信過程對(duì)程序員完全透明般渡。


3脸秽、Java內(nèi)存模型的抽象

??Java線程之間的通信由Java內(nèi)存模型(JMM)控制。JMM決定了一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)另一個(gè)線程可見。從抽象的角度來看雕沿,JMM定義了線程與主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存中辽俗,每一個(gè)線程都有一個(gè)自己私有的本地內(nèi)存朱浴,本地內(nèi)存中存儲(chǔ)了該變量以讀/寫共享變量的副本砰碴。本地內(nèi)存是JMM的一個(gè)抽象概念呈枉,并不真實(shí)存在埃碱,只是為了幫助理解酥泞。

JMM抽象示意圖

從上圖來看,如果線程A和線程B通信的話芝囤,要如下兩個(gè)步驟:
(1)線程A需要將本地內(nèi)存A中的共享變量副本刷新到主內(nèi)存中
(2)線程B去主內(nèi)存讀取線程A之前已更新過的共享變量
步驟示意圖

【舉個(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)存A中修改后的X值刷新到主內(nèi)存去,此時(shí)主內(nèi)存中的X值變?yōu)榱?包券。隨后纫谅,線程B到主內(nèi)存中讀取線程A更新后的共享變量X的值,此時(shí)線程B的本地內(nèi)存的X值也變成了1溅固。

??整體看來付秕,這兩個(gè)步驟是指上是線程A在向線程B發(fā)送消息,而這個(gè)通信過程必須經(jīng)過主內(nèi)存侍郭。JMM通過控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互询吴,來為Java程序員提供內(nèi)存可見性保證。


4亮元、重排序

??在執(zhí)行程序時(shí)為了提高性能猛计,編譯器和處理器常常會(huì)對(duì)指令做重排序,重排序分三類:

(1)編譯器優(yōu)化的重排序
??編譯器在不改變單線程程序語義的前提下爆捞,可以重新安排語句的執(zhí)行順序奉瘤。

(2)指令級(jí)并行的重排序
??現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對(duì)應(yīng)及其指令的執(zhí)行順序盗温。

(3)內(nèi)存系統(tǒng)的重排序
??由于處理器使用緩存和讀/寫緩沖區(qū)藕赞,這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。

??從Java源代碼到最終實(shí)際執(zhí)行的指令序列卖局,會(huì)分別經(jīng)歷下面三種重排序:


??注意斧蜕,所有的重排都會(huì)遵循as-if-serial語義(詳見[Java多線程編程之四] CPU緩存和內(nèi)存屏障),即重排后指令的對(duì)單線程來說跟重排前的指令的執(zhí)行效果是一樣的砚偶,但是該語義不能保證程序指令在多線程環(huán)境下重排后的指令執(zhí)行效果跟重排前一致批销,所以就會(huì)導(dǎo)致可見性問題,所謂可見性問題染坯,簡單地說就是某個(gè)線程修改了某個(gè)變量的值风钻,但是對(duì)另外一個(gè)線程來說,它感知不到這種變化酒请,當(dāng)程序運(yùn)行用到這個(gè)變量時(shí)骡技,用的還是舊值,就會(huì)導(dǎo)致程序運(yùn)行結(jié)果跟我們預(yù)料的大相庭徑羞反。

??上面的這些重排序都可能導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見性問題布朦。

??對(duì)于編譯器,JMM的編譯器重排序規(guī)則會(huì)禁止特定類型的編譯器重排序(即可能導(dǎo)致程序可見性問題的重排序要禁止昼窗,但是不會(huì)禁止能優(yōu)化程序執(zhí)行效率并不影響程序執(zhí)行結(jié)果正確性的重排序)是趴,如下所示:

??A和B的初始值都是0,對(duì)于線程1和線程2來說澄惊,重排序都不會(huì)影響單線程的執(zhí)行效果唆途,但是如果兩個(gè)線程并發(fā)操作A、B的值掸驱,則運(yùn)行結(jié)果可能不一致肛搬,比如重排序前線程2給r1賦值的時(shí)候值為0,但是重排序后毕贼,賦值這個(gè)操作可能在線程1給B賦值1這步之后執(zhí)行温赔,此時(shí)對(duì)線程2來說賦值時(shí)B的值就是1了,而這種重排序就是JMM規(guī)范里要禁止的鬼癣。


??對(duì)于處理器重排序陶贼,JMM的處理器重排序規(guī)則會(huì)要求Java編譯器在生成指令序列時(shí),插入特定類型的內(nèi)存屏障指令待秃,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序(同樣不是所有的處理器重排序都要禁止)拜秧。

??JMM屬于語言級(jí)的內(nèi)存模型,它確保在不同的編譯器和不同的處理器平臺(tái)之上章郁,通過禁止特定類型的編譯器重排序和處理器重排序枉氮,為程序員提供一致的內(nèi)存可見性保證。

處理器重排序

??現(xiàn)代的處理器使用寫緩沖區(qū)來臨時(shí)保存向內(nèi)存寫入的數(shù)據(jù)。寫緩沖區(qū)可以保證指令流水線持續(xù)運(yùn)行嘲恍,它可以避免由于處理器停頓下來等待向內(nèi)存寫入數(shù)據(jù)而產(chǎn)生的延遲。同時(shí)雄驹,通過以批處理的方式刷新寫緩沖區(qū)佃牛,以及合并寫緩沖區(qū)中對(duì)同一內(nèi)存地址的多次寫,可以減少對(duì)內(nèi)存總線的占用医舆。雖然寫緩沖區(qū)有這么多好處俘侠,但每個(gè)處理器上的寫緩沖區(qū),僅僅對(duì)它所在的處理器可見蔬将。這個(gè)特定會(huì)對(duì)內(nèi)存操作的執(zhí)行順序產(chǎn)生重要的影響:處理器對(duì)內(nèi)存的讀/寫操作的執(zhí)行順序爷速,不一定與內(nèi)存實(shí)際發(fā)生的讀/寫操作順序一致。


??示例如下:

??假設(shè)處理器A和處理器B按程序的順序并行執(zhí)行內(nèi)存訪問霞怀,最終卻可能得到x = y = 0惫东。具體的原因如下圖所示:

??處理器A和B同時(shí)把共享變量寫入在寫緩沖區(qū)中(A1、B1)毙石,然后再從內(nèi)存中讀取另一個(gè)共享變量(A2廉沮、B2),最后才把自己寫緩沖區(qū)中保存的臟數(shù)據(jù)刷新到內(nèi)存中(A3徐矩、B3)滞时。當(dāng)以這種時(shí)序執(zhí)行時(shí),程序就可以得到x = y = 0的效果滤灯。

??從內(nèi)存操作實(shí)際發(fā)生的順序來看坪稽,直到處理器A執(zhí)行A3來刷新自己的寫緩存去,寫操作A1才算真正執(zhí)行了鳞骤。雖然處理器A執(zhí)行內(nèi)存操作的順序?yàn)椋篈1 -> A2窒百,但內(nèi)存操作實(shí)際發(fā)生的順序卻是:A2 -> A1。此時(shí)豫尽,處理器A的內(nèi)存操作順序被重排序了贝咙。

??這里的關(guān)鍵是,由于寫緩沖區(qū)僅對(duì)自己的處理器可見拂募,它會(huì)導(dǎo)致處理器執(zhí)行內(nèi)存操作的順序可能會(huì)與內(nèi)存實(shí)際的操作執(zhí)行順序不一致庭猩。由于現(xiàn)代的處理器都會(huì)使用寫緩沖區(qū),因此現(xiàn)代處理器都會(huì)允許對(duì)寫-讀操作重排序陈症。

內(nèi)存屏障指令

??為了保證內(nèi)存可見性蔼水,Java編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來禁止特定類型的處理器重排序。

??處理器提供了兩個(gè)內(nèi)存屏障指令:
(1)讀內(nèi)存屏障:在指令前插入Load Barrier录肯,可以讓高速緩存中的數(shù)據(jù)失效趴腋,強(qiáng)制重新從主內(nèi)存加載數(shù)據(jù),讓CPU緩存與主內(nèi)存保持一致,避免緩存導(dǎo)致的一致性問題优炬。
(2)寫內(nèi)存屏障:在指令后插入Store Barrier颁井,能讓寫入緩存中的最新數(shù)據(jù)更新寫入主內(nèi)存,讓其他線程可見蠢护;當(dāng)發(fā)生這種強(qiáng)制寫入主內(nèi)存的顯式調(diào)用雅宾,CPU就不會(huì)處于性能優(yōu)化考慮進(jìn)行指令重排。

??程序進(jìn)行讀寫時(shí)葵硕,指令執(zhí)行順序可能有:讀寫眉抬,寫讀,讀讀懈凹,寫寫蜀变,所以JMM把內(nèi)存屏障指令分為下列四類:



5、多線程編程中常見的問題

??對(duì)于新手介评,多線程編程不是個(gè)容易上手的技術(shù)库北,對(duì)于經(jīng)驗(yàn)豐富的老手,還時(shí)不時(shí)馬失前蹄们陆,都會(huì)遇到很多問題贤惯,正因?yàn)檫@些問題,所以《Java語言規(guī)范》要提出一些規(guī)則范式來解決這些問題棒掠,常見的問題主要有:

  • 所見非所得
  • 無法用肉眼去檢測程序的準(zhǔn)確性
  • 不同的運(yùn)行平臺(tái)有不同的表現(xiàn)
  • 錯(cuò)誤很難重現(xiàn)

??下面通過一個(gè)實(shí)例程序體會(huì)下這些問題:

public class Demo1Visibility {
    int i = 0;
    boolean isRunning = true;
    
    public static void main(String[] args) throws InterruptedException {
        Demo1Visibility demo = new Demo1Visibility();
        
        new Thread(new Runnable() {
            
            @Override
            public void run() {
                System.out.println("hrer i am...");
                while (demo.isRunning) {
                    demo.i++;
                }
                System.out.println(demo.i);
            }
        }).start();
        
        Thread.sleep(3000L);
        demo.isRunning = false;
        System.out.println(demo.i);
        System.out.println("shutdown...");
    }
}

??程序的邏輯很簡單孵构,有兩個(gè)線程:主線程和匿名線程,程序啟動(dòng)時(shí)烟很,主線程中會(huì)啟動(dòng)匿名線程颈墅,接著主線程休眠3秒,由于demo.isRunning的初始值為true雾袱,因此匿名線程中的while循環(huán)會(huì)不斷循環(huán)恤筛,直到主線程休眠結(jié)束后,將demo.isRunning的值更新為false芹橡,此時(shí)匿名線程的while循環(huán)會(huì)結(jié)束毒坛,從而打印出demo.i的值。
??但是程序運(yùn)行后林说,發(fā)現(xiàn)運(yùn)行結(jié)果并不像預(yù)料的那樣煎殷,程序并沒有打印出i的值,并且一直處于運(yùn)行狀態(tài)腿箩,很明顯這是因?yàn)槟涿€程沒有感知到demo.isRunning的變化豪直,導(dǎo)致一直循環(huán)中,如下所示:


??如果運(yùn)行用的是32位的JDK珠移,并且設(shè)置程序的啟動(dòng)VM參數(shù)-client(默認(rèn)是-server)弓乙,則運(yùn)行結(jié)果如下:

??對(duì)于上面的程序末融,經(jīng)過試驗(yàn)可以知道,JDK暇韧、VM參數(shù)對(duì)程序運(yùn)行的影響如下:

參數(shù) 32位JDK 64位JDK
-server 不打印i的值 不打印i的值
-client 打印i的值 不打印i的值

??從上面的示例可以看出勾习,程序運(yùn)行的結(jié)果不一定如我們預(yù)料的(所見非所得),在不同版本的JDK下運(yùn)行有差異(不同的運(yùn)行平臺(tái)有不同的表現(xiàn))懈玻,使用不同的啟動(dòng)參數(shù)-client/-server有不同的效果巧婶,無法肉眼檢測程序的準(zhǔn)確性,而這些問題就是《Java語言規(guī)范》提供的Java內(nèi)存模型所要解決的問題酪刀,但是要注意Java內(nèi)存模型并不會(huì)實(shí)際解決這些問題,更多的它是一種規(guī)范钮孵、規(guī)則骂倘,而JVM會(huì)去真正實(shí)現(xiàn)這些規(guī)則規(guī)范。

??對(duì)于上面代碼巴席,在非32位JDK非-client下历涝,子線程都不能正常退出while循環(huán)打印出i的值,我們結(jié)合上述講到的Java內(nèi)存模型的示意圖來分析漾唉。

??首先demo是一個(gè)對(duì)象荧库,對(duì)象是存儲(chǔ)在堆內(nèi)存中的,從運(yùn)行時(shí)數(shù)據(jù)區(qū)的示意圖可知赵刑,堆內(nèi)存是所有線程共享的區(qū)域分衫;程序中有主線程和子線程兩個(gè)線程,每個(gè)線程在運(yùn)行時(shí)JVM都會(huì)分配一塊線程私有的內(nèi)存塊般此,我們稱之為線程工作區(qū)蚪战,線程工作區(qū)就會(huì)存儲(chǔ)線程運(yùn)行中需要的局部變量表、操作數(shù)棧铐懊、動(dòng)態(tài)鏈接邀桑、返回地址等信息。主線程會(huì)修改demo.isRunning的值為false科乎,會(huì)將修改內(nèi)容寫入到共享堆內(nèi)存中壁畸,而子線程會(huì)去讀取堆內(nèi)存中的demo.isRunning的值,來讓程序退出while循環(huán)茅茂。

??當(dāng)執(zhí)行指令時(shí)捏萍,需要將其加載到CPU中,程序運(yùn)行中需要存儲(chǔ)一些變量空闲,這些變量會(huì)保存在RAM內(nèi)存中照弥,所以線程工作區(qū)既分布在CPU也分布在RAM內(nèi)存中。從[Java多線程編程之四] CPU緩存和內(nèi)存屏障可知进副,由于內(nèi)存讀取寫入操作的數(shù)據(jù)遠(yuǎn)遠(yuǎn)跟不上CPU運(yùn)行的速度这揣,所以在CPU和內(nèi)存中間悔常,有個(gè)高速緩存,內(nèi)存把數(shù)據(jù)加載到高速緩存中给赞,供CPU讀取机打,當(dāng)CPU要修改內(nèi)存時(shí),同樣是要先寫到高速緩存中片迅,再由高速緩存同步到內(nèi)存中残邀,高速緩存協(xié)議又保證了內(nèi)存中的數(shù)據(jù)被修改時(shí)同樣也會(huì)同步到其他線程的高速緩存中,如圖所示:


??從上一節(jié)對(duì)重排序的介紹可知柑蛇,主線程去寫data時(shí)芥挣,不會(huì)馬上寫入內(nèi)存中,而是先寫入緩存再寫入內(nèi)存中耻台,同步到內(nèi)存中后空免,高速緩存協(xié)議會(huì)將修改同步到子線程的緩存中,這中間有一定的時(shí)延盆耽,所以子線程得過一段時(shí)間(這個(gè)時(shí)間其實(shí)很短蹋砚,肉眼感受不到,但確實(shí)存在)摄杂,按理說子線程應(yīng)該在稍等一段時(shí)間后在while循環(huán)里讀取到data的最新值坝咐,然后退出循環(huán),打印demo.i的值析恢,但是實(shí)際上卻沒有墨坚,這是怎么回事?

??這里又不得不再次提到上面說到的編譯優(yōu)化重排序映挂,在 [Java多線程編程之一] Java代碼是怎么運(yùn)行起來的框杜?看完這篇你就懂了!中提到Java的解釋執(zhí)行和編譯執(zhí)行袖肥,解釋執(zhí)行指JVM在讀取字節(jié)碼執(zhí)行時(shí)吟税,是由執(zhí)行引擎的解釋器逐條將字節(jié)碼翻譯成機(jī)器可識(shí)別的指令懊亡,編譯執(zhí)行則是直接將一段字節(jié)碼翻譯成機(jī)器可以識(shí)別的指令碼知牌。

??說起Java的編譯執(zhí)行就不得不提到JIT編譯器(Just In Time Compiler)赶盔,當(dāng)Java程序中某個(gè)方法不斷被調(diào)用(比如遞歸)或者某段代碼不斷被循環(huán)(比如while(true))時(shí),調(diào)用執(zhí)行的頻率達(dá)到一定水平就會(huì)升級(jí)為熱點(diǎn)代碼寸癌,這時(shí)啟動(dòng)JIT編譯专筷,直接將熱點(diǎn)代碼編譯成機(jī)器碼放到方法區(qū)中,當(dāng)程序再次執(zhí)行到這段熱點(diǎn)代碼時(shí)蒸苇,直接從方法區(qū)中取機(jī)器指令執(zhí)行磷蛹,從而提升程序執(zhí)行的效率,在JIT編譯時(shí)溪烤,會(huì)進(jìn)行指令重排做性能優(yōu)化味咳,指令重排不僅僅是對(duì)執(zhí)行指令的重排庇勃,程序的邏輯可能也會(huì)發(fā)生改變,比如上面子線程執(zhí)行的循環(huán)體槽驶,可能被優(yōu)化成下面的形式:

boolean f = demo.isRunning;
if (f) {
    while (true) {
        i++;
    }
}

??這相當(dāng)于demo.isRunning一開始就被緩存起來了责嚷,并且不會(huì)再次去讀取它的值,這就導(dǎo)致了主線程修改了demo.isRunning時(shí)掂铐,子線程感受不到罕拂,所以一直在循環(huán)執(zhí)行i++,上面提到的運(yùn)行VM參數(shù)-client全陨、-server屬于JIT編譯的參數(shù)爆班,影響指令優(yōu)化重排的行為,所以在32位JDK下辱姨,設(shè)置不同的參數(shù)會(huì)有不同的效果柿菩。

??問題的原因找到了,如何解決這個(gè)問題炮叶?很簡單碗旅,定義isRunning時(shí)用volatile修飾即可渡处,volatile有禁止指令重排的效果镜悉,如下所示:


??將使用javap編譯字節(jié)碼,可以看到對(duì)應(yīng)的文本描述医瘫,從中可以看到對(duì)isRunning的描述多了一個(gè)標(biāo)志ACC_VOLATILE

??從官方文檔可以看出侣肄,加了volatile描述的字段是不能被緩存的,因此加了volatile描述的字段醇份,被多個(gè)不同線程訪問時(shí)稼锅,都是直接去內(nèi)存中查找,而不會(huì)加一層緩存僚纷,這保證了可見性矩距。



6、Volatile關(guān)鍵字

??上面問題分析寫了一堆怖竭,最終卻出人意料地讓一個(gè)volatile關(guān)鍵字輕松解決锥债,volatile有什么魔力?

??volatile可以解決多線程環(huán)境中共享數(shù)據(jù)的可見性問題痊臭,即一個(gè)線程修改了共享變量時(shí)哮肚,其他線程馬上能夠感知到這種變化。

舉個(gè)例子:

public class VolatileTest {
    volatile long a = 1L;       // 使用volatile聲明的64位long型
    
    public void set(long l) {   // 單個(gè)volatile變量的寫
        a = l;
    }
    
    public long get() {         // 單個(gè)volatile變量的讀
        return a;
    }
    
    public void getAndIncreament() {
        a++;                    // 復(fù)合多個(gè)volatile變量的讀/寫                   
    }
}

假設(shè)有多個(gè)線程分別調(diào)用上面程序的三個(gè)方法广匙,則這個(gè)程序在語義上和下面的程序等價(jià):

public class VolatileTest {
    long a = 1L;        // 64位的long型普通變量
    
    public synchronized void set(long l) {
        a = l;          // 對(duì)單個(gè)普通變量的寫用同一個(gè)鎖同步
    }
    
    public synchronized long get() {
        return a;
    }
    
    public void getAndIncreament() {
        long temp = get();
        temp += 1L;
        set(temp);
    }
}

??如上面示例程序所示允趟,對(duì)一個(gè)volatile變量的單個(gè)讀/寫操作,與對(duì)一個(gè)普通變量的讀/寫操作使用同一個(gè)鎖來同步鸦致,執(zhí)行效果是相同的潮剪。

??鎖的語義決定了臨界區(qū)代碼的執(zhí)行具有原子性涣楷,這意味著不管是什么類型的變量,只要它是volatile變量鲁纠,對(duì)該變量的讀寫就將具有原子性总棵,但是這種原子性是對(duì)單個(gè)變量的操作而言的,如果是多個(gè)volatile操作或類似于volatile++這種復(fù)合操作改含,則其整體上不具有原子性情龄。

??再回到5、多線程編程中的程序案例捍壤,如果對(duì)demo.isRunning修飾了volatile骤视,禁止指令重排,JIT不會(huì)優(yōu)化出下面這種形式的代碼鹃觉,程序讀寫時(shí)雖然也用到高速緩存专酗,但是保證了多個(gè)線程對(duì)同個(gè)共享數(shù)據(jù)的同步可見。

boolean f = demo.isRunning;
if (f) {
    while (true) {
        i++;
    }
}

??總結(jié)起來:volatile變量自身具有下列特性:

  • 可見性:對(duì)一個(gè)volatile變量的讀盗扇,總是能看到(任意線程)對(duì)這個(gè)volatile變量最后的寫入祷肯。
  • 原子性:對(duì)任意單個(gè)volatile變量的讀/寫具有原子性,但類似于volatile++這種復(fù)合操作不具有原子性疗隶。

(1)volatile寫-讀的內(nèi)存定義

  • 當(dāng)寫一個(gè)volatile時(shí)佑笋,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到內(nèi)存。
  • 當(dāng)讀一個(gè)volatile時(shí)斑鼻,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量置為無效蒋纬,線程接下來將從主內(nèi)存中讀取共享變量。

(2)volatile內(nèi)存語義的實(shí)現(xiàn)

下面是JMM針對(duì)編譯器制定的volatile重排序規(guī)則表:



為了實(shí)現(xiàn)volatile的內(nèi)存語義坚弱,編譯器在生成字節(jié)碼時(shí)蜀备,會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。

下面是基于保守策略的JMM內(nèi)存屏障插入策略:

  • 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障荒叶。
  • 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障碾阁。
  • 在每個(gè)volatile讀操作的前面插入一個(gè)LoadLoad屏障。
  • 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障些楣。

下面是保守策略下脂凶,volatile寫操作插入內(nèi)存屏障后生成的指令序列示意圖:


volatile寫插入內(nèi)存屏障示意圖

下面是保守策略下,volatile讀操作插入內(nèi)存屏障后生成的指令序列示意圖:

volatile讀插入內(nèi)存pingzhag

上述volatile寫操作和讀操作的內(nèi)存屏障插入策略非常保守戈毒。在實(shí)際執(zhí)行時(shí)艰猬,只要不改變volatile寫 - 讀的內(nèi)存語義,編譯器可以根據(jù)具體情況省略不必要的屏障埋市。




三冠桃、Java內(nèi)存模型中的一些語義和規(guī)則

1、as-if-serial語義

??不管怎么重排序(編譯器和處理器為了提高并行度)道宅,單線程程序的執(zhí)行結(jié)果不能被改變食听,編譯器胸蛛、runtime和處理器都必須遵循as-if-serial語義,也就是說樱报,編譯器和處理器不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作做重排序葬项。


2、Shaerd Variables定義

??可以在線程之間共享的內(nèi)存稱為共享內(nèi)存或堆內(nèi)存迹蛤。所有實(shí)例字段民珍、靜態(tài)字段數(shù)組元素都存儲(chǔ)在堆內(nèi)存中,這些字段和數(shù)組都是共享變量盗飒。

??多個(gè)線程對(duì)共享變量的訪問操作中嚷量,如果至少有一個(gè)訪問時(shí)寫操作,那么對(duì)同一個(gè)變量的兩次訪問時(shí)沖突的逆趣,訪問順序的不一樣可能導(dǎo)致出現(xiàn)不同的結(jié)果蝶溶。


3、線程間操作的定義

(1)線程間操作指:一個(gè)程序執(zhí)行的操作可被其他線程感知或被其他線程直接影響宣渗。
(2)Java內(nèi)存模型只描述線程間操作抖所,不描述線程內(nèi)操作,線程內(nèi)操作按照線程內(nèi)語義執(zhí)行痕囱。

線程間操作有

  • 普通讀
  • 普通寫
  • volatile讀
  • volatile寫
  • Lock田轧、Unlock:加鎖解鎖通常發(fā)生在多個(gè)線程對(duì)共享變量進(jìn)行操作的同步。
  • 線程的第一個(gè)和最后一個(gè)操作:簡單說就是一個(gè)線程的啟動(dòng)和終止能被其他線程感知到咐蝇。
  • 外部操作:比如多個(gè)線程去訪問DB涯鲁,DB是外部的資源巷查,所以叫外部操作有序,也是線程間操作。


4岛请、同步規(guī)則的定義

(1) 對(duì)volatile變量v的寫入旭寿,與所有其他線程后續(xù)對(duì)v的讀同步
(2) 對(duì)于監(jiān)視器m的解鎖與所有后續(xù)操作對(duì)m的加鎖同步

這里有兩層語義:一層是加鎖解鎖操作是不能被重排序的;


另一層含義線程1拿到鎖做了一些操作(比如修改了某個(gè)變量的值)崇败,接著解鎖盅称,接下來線程2拿到鎖,此時(shí)線程2是可以感知到線程1在持有鎖期間的操作后室。


(3)對(duì)于每個(gè)屬性寫入默認(rèn)值(0缩膝,false, null)與每個(gè)線程對(duì)其進(jìn)行的操作同步

??對(duì)象創(chuàng)建時(shí),JVM會(huì)根據(jù)對(duì)象屬性類型等信息為其分配一塊內(nèi)存岸霹,由于內(nèi)存中可能存在臟數(shù)據(jù)疾层,所以JVM在創(chuàng)建對(duì)象時(shí),會(huì)根據(jù)其屬性類型初始化默認(rèn)值贡避,比如數(shù)字類型的初始化為0痛黎,布爾類型初始化為false予弧,對(duì)象類型初始化為null,這個(gè)初始化的操作在線程訪問對(duì)象之前完成湖饱,確保訪問對(duì)象的線程不會(huì)看到“臟數(shù)據(jù)”(即沒初始化之前的內(nèi)存亂碼)掖蛤。

(4)啟動(dòng)線程的操作與線程中的第一個(gè)操作同步

??這里同步的意思同樣有兩層含義,一層指線程要先被啟動(dòng)才能執(zhí)行線程里的run方法井厌;一層指啟動(dòng)線程的線程調(diào)用start()方法啟動(dòng)了線程蚓庭,這個(gè)啟動(dòng)動(dòng)作修改了被啟動(dòng)的線程狀態(tài),并且被啟動(dòng)線程能感知到這種狀態(tài)的變化仅仆。


(5)線程T2的最后操作與線程T1發(fā)現(xiàn)線程T2已經(jīng)結(jié)束同步(isAlive,join可以判斷線程是否終結(jié))

??跟上面的規(guī)則差不多彪置,就是T2線程操作結(jié)束時(shí),線程狀態(tài)會(huì)修改成Terminated蝇恶,這個(gè)線程狀態(tài)對(duì)其他線程是可見的拳魁。

(6)如果線程T1中斷了T2,那么線程T1的中斷操作與其他所有線程發(fā)現(xiàn)T2被中斷了同步撮弧,通過拋出InterruptedException異常潘懊,或者調(diào)用Thread.interrupted或Thread.isInterrupted

??線程T1調(diào)用t1.interrupted()來中斷線程T1,本質(zhì)是修改T1線程對(duì)象的interrupted屬性的狀態(tài)值為true贿衍,這個(gè)狀態(tài)值對(duì)其他線程可見授舟,其他線程發(fā)現(xiàn)線程T2中斷肯定在線程T1中斷T2之后發(fā)生


5、Happens-before先行發(fā)生原則

??happens-before關(guān)系用于描述兩個(gè)有沖突的動(dòng)作之間的順序贸辈,如果一個(gè)action happends before另一個(gè)action释树,則第一個(gè)操作被第二個(gè)操作可見,JVM需要實(shí)現(xiàn)如下happens-before規(guī)則:

(1)某個(gè)線程中的每個(gè)動(dòng)作都happens-before該線程中該動(dòng)作后面的動(dòng)作

??簡單地說就是代碼指令的順序執(zhí)行

(2)某個(gè)管程上的unlock動(dòng)作happens-before同一個(gè)管程上后續(xù)的lock操作

??先加鎖后解鎖擎淤,順序不可調(diào)整

(3)對(duì)某個(gè)volatile字段的寫操作happens-before每個(gè)后續(xù)對(duì)該volatile字段的讀操作

(4)在某個(gè)線程對(duì)象上調(diào)用start()方法happens-before該啟動(dòng)線程中的任意動(dòng)作

(5)如果在線程t1中成功執(zhí)行了t2.join()奢啥,則t2中所有操作對(duì)t1可見

(6)如果某個(gè)動(dòng)作a happens-before動(dòng)作b,且b happens-before動(dòng)作c嘴拢,則有a happens-before c


6桩盲、final在JMM中的處理

??比如線程1創(chuàng)建了下面的Demo2Final類的對(duì)象,線程1對(duì)對(duì)象屬性x席吴、y的修改赌结,只有x可以保證能被線程2讀取到正確的版本3,因?yàn)閤被final所修飾孝冒;但是y不一定能被讀取到正確的構(gòu)造版本柬姚,線程2讀取y可能讀到的是0。

public class Demo2Final {
    final int x;
    int y;
    
    static Demo2Final f;
    
    public Demo2Final() {
        x = 3;
        y = 4;
    }
    
    static void writer() { f = new Demo2Final(); }
    
    static void reader() {
        if (f != null) {
            int i = f.x;    // 一定會(huì)讀到正確的構(gòu)造版本
            int j = f.y;    // 可能會(huì)讀到默認(rèn)值0
            System.out.println("i = " + i + ", j = " + j);
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        // Thread1 writer
        // Thread2 read
    } 
}



??比如線程1創(chuàng)建了下面Demo3Final對(duì)象庄涡,在構(gòu)造函數(shù)中量承,被聲明為final的屬性x先初始化,再用x來給y賦值,則y被初始化后的值也可以被線程2看到宴合,而線程2可能依然看不到屬性c正確的構(gòu)造版本焕梅。

public class Demo3Final {
    final int x;
    int y;
    int c;
    
    static Demo3Final f;
    
    public Demo3Final() {
        x = 3;
        // #### 重點(diǎn)語句  ####
        y = x;    // 因?yàn)閤被final修飾了,所以可讀到y(tǒng)的正確構(gòu)造版本
        c = 4;
    }
    
    static void writer() { f = new Demo3Final(); }
    
    static void reader() {
        if (f != null) {
            int i = f.x;
            int j = f.y;
            int k = f.c;
            System.out.println("i = " + i + ", j = " + j + ", k = " + k);
        }
    }
    
    public static void main(String[] args) {
        // Thread1 write
        Demo3Final.writer();
        
        // Thread2 read
    }
}






7卦洽、Word Tearing字節(jié)處理

??有些處理器(尤其是早期的Alphas處理器)沒有提供寫單個(gè)字節(jié)的功能贞言。在這樣的處理器上更新byte數(shù)組,若只是簡單地讀取整個(gè)內(nèi)容阀蒂,更新對(duì)應(yīng)的字節(jié)该窗,然后將整個(gè)內(nèi)容再寫回內(nèi)存,將是不合法的蚤霞。

??這個(gè)問題有時(shí)候被稱為“字分裂(word tearing)”酗失,更新單個(gè)字節(jié)有難度的處理器,就需要尋求其他方式來解決問題昧绣。因此规肴,編程人員需要注意,盡量不要對(duì)byte[]中的元素進(jìn)行重新賦值夜畴,更不要在多線程程序中這樣做拖刃。

如下圖所示,展示了字分類的問題:

??內(nèi)存中存在數(shù)組[10, 2, 6, 9, 11, 23, 14]贪绘,現(xiàn)在線程1要修改數(shù)組下標(biāo)為4的元素值為10兑牡,線程2要修改數(shù)組下標(biāo)為3的元素值為6。


??但是由于處理器無法寫單個(gè)字節(jié)税灌,所以線程t1會(huì)拷貝整個(gè)數(shù)組的內(nèi)容到線程內(nèi)存中均函,對(duì)下標(biāo)為4的元素進(jìn)行賦值,再重新寫回內(nèi)存中去菱涤,線程t2也是類似操作苞也,但是這里存在一個(gè)問題,由于對(duì)數(shù)組進(jìn)行寫操作是整個(gè)數(shù)組進(jìn)行的狸窘,所以最后數(shù)組要么變成t1寫入的數(shù)組墩朦,要么變成t2寫入的數(shù)據(jù)坯认,這兩個(gè)都會(huì)到時(shí)另一個(gè)線程的修改被抹除掉了翻擒,如下所示:





8、double和long的特殊處理

??由于《Java語言規(guī)范》的原因牛哺,對(duì)非volatile的double陋气、long的單詞寫操作是分兩次來進(jìn)行的,每次操作其中32位引润,這可能導(dǎo)致第一次寫入后巩趁,讀取的值是臟數(shù)據(jù),第二次寫完成后,才能讀到正確值议慰。但是現(xiàn)在的JVM大都針對(duì)這點(diǎn)進(jìn)行了優(yōu)化蠢古,使得對(duì)double、long類型數(shù)據(jù)的操作都能以整體64位進(jìn)行别凹,保持原子性草讶,防止讀取的線程讀到臟數(shù)據(jù)。



??讀寫volatile修飾的long炉菲、double是原子性的堕战。

??商業(yè)JVM不會(huì)存在這個(gè)問題,雖然規(guī)范沒要求實(shí)現(xiàn)原子性拍霜,但是考慮到實(shí)際應(yīng)用嘱丢,大部分都實(shí)現(xiàn)了原子性。

??《Java語言規(guī)范》中說道:建議程序員將共享的64位值(long祠饺、double)用volatile修飾或正確同步其程序以避免可能的復(fù)雜的情況越驻。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市道偷,隨后出現(xiàn)的幾起案子伐谈,更是在濱河造成了極大的恐慌,老刑警劉巖试疙,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诵棵,死亡現(xiàn)場離奇詭異,居然都是意外死亡祝旷,警方通過查閱死者的電腦和手機(jī)履澳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來怀跛,“玉大人距贷,你說我怎么就攤上這事∥悄保” “怎么了忠蝗?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長漓拾。 經(jīng)常有香客問我阁最,道長,這世上最難降的妖魔是什么骇两? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任速种,我火速辦了婚禮,結(jié)果婚禮上低千,老公的妹妹穿的比我還像新娘配阵。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布棋傍。 她就那樣靜靜地躺著救拉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪瘫拣。 梳的紋絲不亂的頭發(fā)上近上,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音拂铡,去河邊找鬼壹无。 笑死,一個(gè)胖子當(dāng)著我的面吹牛感帅,可吹牛的內(nèi)容都是我干的斗锭。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼失球,長吁一口氣:“原來是場噩夢啊……” “哼岖是!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起实苞,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤豺撑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后黔牵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體聪轿,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年猾浦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了陆错。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡金赦,死狀恐怖音瓷,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情夹抗,我是刑警寧澤绳慎,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站漠烧,受9級(jí)特大地震影響杏愤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜沽甥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一声邦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧摆舟,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至照宝,卻和暖如春蛇受,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背厕鹃。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工兢仰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人剂碴。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓把将,卻偏偏與公主長得像,于是被迫代替她去往敵國和親忆矛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子察蹲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容