Java內(nèi)存模型

1、主內(nèi)存與工作內(nèi)存

Java內(nèi)存模型的主要目的是定義程序中各種變量的訪問規(guī)則,即關(guān)注再虛擬機(jī)中把變量值存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量值這樣的底層細(xì)節(jié)。此處的變量(Variables)只包括實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,不包括局部變量與方法參數(shù)。

Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存(Main Memory)中。每條線程還有自己的工作內(nèi)存(Working Memory),線程的工作內(nèi)存中保存了被該線程使用的變量的主內(nèi)存副本,線程對(duì)變量的所有操作(讀取、賦值等)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的數(shù)據(jù)。不同的線程之間也無法直接訪問對(duì)方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過主內(nèi)存來完成。線程、主內(nèi)存、工作內(nèi)存三者的交互關(guān)系如下圖所示:

2、內(nèi)存間交互操作

關(guān)于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,即一個(gè)變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步回主內(nèi)存這一類的實(shí)現(xiàn)細(xì)節(jié),Java內(nèi)存模型中定義了以下8種操作來完成。Java虛擬機(jī)實(shí)現(xiàn)時(shí)必須保證下面提及的每一種操作都是原子的、不可再分的。

1)lock(鎖定):作用于主內(nèi)存的變量,它把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占的狀態(tài)。

2)unlock(解鎖):作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。

3)read(讀?。鹤饔糜谥鲀?nèi)存的變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用。

4)load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。

5)use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。

6)assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。

7)store(存儲(chǔ)):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write操作使用。

8)write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。

如果要把一個(gè)變量從主內(nèi)存拷貝到工作內(nèi)存,那就要按順序執(zhí)行read和load操作,如果要把變量從工作內(nèi)存同步回主內(nèi)存,就要按順序執(zhí)行store和write操作。除此之外,Java內(nèi)存模型還規(guī)定了在執(zhí)行上述8種基本操作時(shí)必須滿足如下規(guī)則:

1)不允許read和load、store和write操作之一單獨(dú)出現(xiàn),即不允許一個(gè)變量從主內(nèi)存讀取了但工作內(nèi)存不接受,或者工作內(nèi)存發(fā)起回寫了但主內(nèi)存不接受的情況出現(xiàn)。

2)不允許一個(gè)線程丟棄它最近的assign操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存。

3)不允許一個(gè)線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存中。

4)一個(gè)新的變量只能在主內(nèi)存中“誕生”,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量,換句話說就是對(duì)一個(gè)變量實(shí)施use、store操作之前,必須先執(zhí)行assign和load操作。

5)一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會(huì)被解鎖。

6)如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,那將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行l(wèi)oad或assign操作以初始化變量的值。

7)如果一個(gè)變量事先沒有被lock操作鎖定,那就不允許對(duì)它執(zhí)行unlock操作,也不允許去unlock一個(gè)被其他線程鎖定的變量。

8)對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)。

3、對(duì)于volatile型變量的特殊規(guī)則

關(guān)鍵字volatile可以說時(shí)Java虛擬機(jī)提供的最輕量級(jí)的同步機(jī)制,但是它并不容易被正確、完整地理解,以至于許多程序員都習(xí)慣去避免使用它,遇到需要處理多線程數(shù)據(jù)競爭問題地時(shí)候一律使用synchronized來進(jìn)行同步。

Java內(nèi)存模型為volatile專門定義了一些特殊地訪問規(guī)則。假定T表示一個(gè)線程,V和W分別表示兩個(gè)volatile型變量,那么在進(jìn)行read、load、use、assign、store和write操作時(shí)需要滿足如下規(guī)則:

1)只有當(dāng)線程T對(duì)變量V執(zhí)行地前一個(gè)動(dòng)作是load的時(shí)候,線程T才能對(duì)變量V執(zhí)行use動(dòng)作;并且,只有當(dāng)線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是use的時(shí)候,線程T才能對(duì)變量V執(zhí)行l(wèi)oad動(dòng)作。線程T對(duì)變量V的use動(dòng)作可以認(rèn)為是和線程T對(duì)變量V的load、read動(dòng)作相關(guān)聯(lián)的,必須連續(xù)且一起出現(xiàn)。

這條規(guī)則要求在工作內(nèi)存中,每次使用V前都必須先從主內(nèi)存刷新最新的值,用于保證能看見其他線程對(duì)變量V所做的修改。

2)只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)作是assign的時(shí)候,線程T才能對(duì)變量V執(zhí)行store動(dòng)作;并且,只有當(dāng)線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是store的時(shí)候,線程T才能對(duì)變量V執(zhí)行assign動(dòng)作。線程T對(duì)變量V的assign動(dòng)作可以認(rèn)為是和線程T對(duì)變量V的store、write動(dòng)作相關(guān)聯(lián)的,必須連續(xù)且一起出現(xiàn)。

這條規(guī)則要求在工作內(nèi)存中,每次修改V后都必須立刻同步回主內(nèi)存中,用于保證其他線程可以看到自己對(duì)變量V所做的修改。

3)假定動(dòng)作A是線程T對(duì)變量V實(shí)施的use或assign動(dòng)作,假定動(dòng)作F是和動(dòng)作A相關(guān)聯(lián)的load或store動(dòng)作,假定動(dòng)作P是和動(dòng)作F相應(yīng)的對(duì)變量V的read或write動(dòng)作;與此類似,假定動(dòng)作B是線程T對(duì)變量W實(shí)施的use或assign動(dòng)作,假定動(dòng)作G是和動(dòng)作B相關(guān)聯(lián)的load或store動(dòng)作,假定動(dòng)作Q是和動(dòng)作G相應(yīng)的對(duì)變量W的read或write動(dòng)作。如果A先于B,那么P先于Q。

這條規(guī)則要求volatile修飾的變量不能被指令重排序優(yōu)化,從而保證代碼的執(zhí)行順序與程序的順序相同。

簡單來講就是:

1)volatile關(guān)鍵字保證變量對(duì)所有線程的可見性。

2)volatile關(guān)鍵字禁止指令重排。

3)volatile關(guān)鍵字不能保證變量的原子性。

注:不保證原子性的原因是從主內(nèi)存讀取到寫回主內(nèi)存這段時(shí)間內(nèi)變量是存在工作內(nèi)存可能在被線程操作。

保證原子性可以使用synchronized、java.util.concurrent中的鎖或原子類。

4、原子性、可見性與有序性

1)原子性

由Java內(nèi)存模型來直接保證的原子性變量操作包括read、load、assign、use、store和write這六個(gè), 我們大致可以認(rèn)為,基本數(shù)據(jù)類型的訪問、讀寫都是具備原子性的。

如果應(yīng)用場景需要一個(gè)更大范圍的原子性保證,Java內(nèi)存模型還提供了lock和unlock操作來滿足這種需求,盡管虛擬機(jī)未把lock和unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令monitorenter和monitorexit來隱式地使用這兩個(gè)操作。這兩個(gè)字節(jié)碼指令反映到Java代碼中就是同步塊——synchronized關(guān)鍵字,因此在synchronized塊之間的操作也具備原子性。

2)可見性

可見性就是指當(dāng)一個(gè)線程修改了共享變量的值時(shí),其他線程能夠立即得知這個(gè)修改。Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi) 存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實(shí)現(xiàn)可見性的,無論是 普通變量還是volatile變量都是如此。普通變量與volatile變量的區(qū)別是,volatile的特殊規(guī)則保證了新值 能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。因此我們可以說volatile保證了多線程操作 時(shí)變量的可見性,而普通變量則不能保證這一點(diǎn)。

除了volatile之外,Java還有兩個(gè)關(guān)鍵字能實(shí)現(xiàn)可見性,它們是synchronized和final。同步塊的可見性是由“對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)”這條規(guī)則獲得的。而final關(guān)鍵字的可見性是指:被final修飾的字段在構(gòu)造器中一旦被初始化完成,并且構(gòu)造器沒有把“this”的引用傳遞出去,那么在其他線程中就能看見final字段的值。

3)有序性

Java程序中天然的有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有的操作都是有序的;如果在一個(gè)線程中觀察另一個(gè)線程, 所有的操作都是無序的。前半句是指“線程內(nèi)似表現(xiàn)為串行的語義”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。

Java語言提供了volatile和synchronized兩個(gè)關(guān)鍵字來保證線程之間操作的有序性,volatile關(guān)鍵字本 身就包含了禁止指令重排序的語義,而synchronized則是由“一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì) 其進(jìn)行l(wèi)ock操作”這條規(guī)則獲得的,這個(gè)規(guī)則決定了持有同一個(gè)鎖的兩個(gè)同步塊只能串行地進(jìn)入。

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

Java內(nèi)存模型中存在一些”天然的“先行發(fā)生關(guān)系,這些先行發(fā)生關(guān)系無須任何同步器協(xié)助就已經(jīng)存在,可以在編碼中直接使用。如果兩個(gè)操作之間的關(guān)系不在此列,并且無法從下列規(guī)則推導(dǎo)出來,則它們就沒有順序性保障,虛擬機(jī)可以對(duì)它們隨意地進(jìn)行重排序。

1)程序次序規(guī)則(Program Order Rule):在一個(gè)線程內(nèi),按照控制流順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。注意,這里說的是控制流順序而不是程序代碼順序,因?yàn)橐紤]分支、循環(huán)等結(jié)構(gòu)。

2)管程鎖定規(guī)則(Monitor Lock Rule):一個(gè)unlock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作。這里必須強(qiáng)調(diào)的是“同一個(gè)鎖”,而“后面”是指時(shí)間上的先后。

3)volatile變量規(guī)則(Volatile Variable Rule):對(duì)一個(gè)volatile變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作,這里的“后面”同樣是指時(shí)間上的先后。

4)線程啟動(dòng)規(guī)則(Thread Start Rule):Thread對(duì)象的start()方法先行發(fā)生于此線程的每一個(gè)動(dòng)作。

5)線程終止規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對(duì)此線程的終止檢測,我們可以通過Thread::join()方法是否結(jié)束、Thread::isAlive()的返回值等手段檢測線程是否已經(jīng)終止執(zhí)行。

6)線程中斷規(guī)則(Thread Interruption Rule):對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread::interrupted()方法檢測到是否有中斷發(fā)生。

7)對(duì)象終結(jié)規(guī)則(Finalizer Rule):一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開始。

8)傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 注:此文是我在讀完周志明老師的深入理解Java虛擬機(jī)之后總結(jié)的一篇文章,請(qǐng)閱讀此書獲取更加詳細(xì)的信息. 在介紹Ja...
    AlstonWilliams閱讀 651評(píng)論 0 1
  • 一、概述 計(jì)算機(jī)所有的運(yùn)算任務(wù)都不可能只靠處理器計(jì)算就能完成,至少與內(nèi)存的交互,如讀取運(yùn)算數(shù)據(jù)、存儲(chǔ)運(yùn)算結(jié)果等,就...
    落地生涯閱讀 558評(píng)論 0 0
  • 目錄 一、Java 內(nèi)存模型的主要目標(biāo)二、主內(nèi)存和工作內(nèi)存三、內(nèi)存件的交互操作四、對(duì)于 volatile 型變量的...
    panning閱讀 1,763評(píng)論 0 8
  • 2021-11-04 硬件的效率與一致性 “讓計(jì)算機(jī)并發(fā)執(zhí)行若干個(gè)運(yùn)算任務(wù)”與“更充分地利用計(jì)算機(jī)處理器的效能”之...
    lemery閱讀 300評(píng)論 0 0
  • 除了充分利用計(jì)算機(jī)處理器的能力外,一個(gè)服務(wù)端同時(shí)對(duì)多個(gè)客戶端提供服務(wù)則是另一個(gè)更具體的并發(fā)應(yīng)用場景。衡量一個(gè)服務(wù)性...
    胡二囧閱讀 1,458評(píng)論 0 12

友情鏈接更多精彩內(nèi)容