初識Java內(nèi)存模型

Java內(nèi)存模型(JMM)

JMM規(guī)定Java每個線程都有自己的工作內(nèi)存(Working Memory),線程的工作內(nèi)存中有共享變量的副本,共享變量則存放在主存(Main Memory)中。工作內(nèi)存是線程私有的,而主存則是所有線程共享的。工作內(nèi)存用于存放線程私有的數(shù)據(jù)。而Java內(nèi)存模型中規(guī)定所有變量都存儲在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內(nèi)存中進行。首先要將變量從主內(nèi)存拷貝的自己的工作內(nèi)存空間,然后對變量進行操作,操作完成后再將變量寫回主內(nèi)存,不能直接操作主內(nèi)存中的變量,工作內(nèi)存中存儲著主內(nèi)存中的變量副本拷貝,前面說過,工作內(nèi)存是每個線程的私有數(shù)據(jù)區(qū)域,因此不同的線程間無法訪問對方的工作內(nèi)存,線程間的通信(傳值)必須通過主內(nèi)存來完成,其簡要訪問過程如下圖:

java內(nèi)存模型

主內(nèi)存

主要存儲的是Java實例對象,所有線程創(chuàng)建的實例對象都存放在主內(nèi)存中,不管該實例對象是成員變量還是方法中的本地變量(也稱局部變量),當然也包括了共享的類信息、常量、靜態(tài)變量。由于是共享數(shù)據(jù)區(qū)域,多條線程對同一個變量進行訪問可能會發(fā)現(xiàn)線程安全問題。

工作內(nèi)存

主要存放主內(nèi)存變量的副本,工作內(nèi)存是線程私有的。線程讀寫一個變量時首先會在工作內(nèi)存中進行操作,然后再同步到主內(nèi)存中。兩個線程的工作內(nèi)存互不影響。工作內(nèi)存其實相當于線程對主內(nèi)存的一個高速緩存。

區(qū)分主內(nèi)存和工作內(nèi)存后,了解一下主內(nèi)存與工作內(nèi)存的數(shù)據(jù)存儲類型以及操作方式,根據(jù)虛擬機規(guī)范,對于一個實例對象中的成員方法而言,如果方法中包含本地變量是基本數(shù)據(jù)類型(boolean,byte,short,char,int,long,float,double),將直接存儲在工作內(nèi)存的幀棧結(jié)構(gòu)中,但倘若本地變量是引用類型,那么該變量的引用會存儲在功能內(nèi)存的幀棧中,而對象實例將存儲在主內(nèi)存(共享數(shù)據(jù)區(qū)域,堆)中。但對于實例對象的成員變量,不管它是基本數(shù)據(jù)類型或者包裝類型(Integer、Double等)還是引用類型,都會被存儲到堆區(qū)。至于static變量以及類本身相關(guān)信息將會存儲在主內(nèi)存中。需要注意的是,在主內(nèi)存中的實例對象可以被多線程共享,倘若兩個線程同時調(diào)用了同一個對象的同一個方法,那么兩條線程會將要操作的數(shù)據(jù)拷貝一份到自己的工作內(nèi)存中,執(zhí)行完成操作后才刷新到主內(nèi)存,簡單示意圖如下所示:

jmm主存和工作內(nèi)存變量存儲
  • 一個本地變量可能是原始類型,在這種情況下,它總是“呆在”線程棧上。
  • 一個本地變量也可能是指向一個對象的一個引用。在這種情況下,引用(這個本地變量)存放在線程棧上,但是對象本身存放在堆上。
  • 一個對象可能包含方法,這些方法可能包含本地變量。這些本地變量仍然存放在線程棧上,即使這些方法所屬的對象存放在堆上。
  • 一個對象的成員變量可能隨著這個對象自身存放在堆上。不管這個成員變量是原始類型還是引用類型。
    靜態(tài)成員變量跟隨著類定義一起也存放在堆上。
  • 存放在堆上的對象可以被所有持有對這個對象引用的線程訪問。當一個線程可以訪問一個對象時,它也可以訪問這個對象的成員變量。如果兩個線程同時調(diào)用同一個對象上的同一個方法,它們將會都訪問這個對象的成員變量,但是每一個線程都擁有這個成員變量的私有拷貝。

JMM下的線程通信

線程間通信必須要經(jīng)過主內(nèi)存。
如下,如果線程A與線程B之間要通信的話,必須要經(jīng)歷下面2個步驟:

1)線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去。

2)線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量。

jmm下線程通信

關(guān)于主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步到主內(nèi)存之間的實現(xiàn)細節(jié),Java內(nèi)存模型定義了以下八種操作來完成:

  1. lock(鎖定):作用于主內(nèi)存的變量,把一個變量標識為一條線程獨占狀態(tài)。
  2. unlock(解鎖):作用于主內(nèi)存變量,把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
  3. read(讀?。鹤饔糜谥鲀?nèi)存變量,把一個變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動作使用
  4. load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
  5. use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量值傳遞給執(zhí)行引擎,每當虛擬機遇到一個需要使用變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
  6. assign(賦值):作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
  7. store(存儲):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中,以便隨后的write的操作。
  8. write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個變量的值傳送到主內(nèi)存的變量中。

Java內(nèi)存模型還規(guī)定了在執(zhí)行上述八種基本操作時,必須滿足如下規(guī)則:

如果要把一個變量從主內(nèi)存中復制到工作內(nèi)存,就需要按順尋地執(zhí)行read和load操作, 如果把變量從工作內(nèi)存中同步回主內(nèi)存中,就要按順序地執(zhí)行store和write操作。但Java內(nèi)存模型只要求上述操作必須按順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行。

  • 不允許read和load、store和write操作之一單獨出現(xiàn)
  • 不允許一個線程丟棄它的最近assign的操作,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中。
  • 不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中。
  • 一個新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操作之前,必須先執(zhí)行過了assign和load操作。
  • 一個變量在同一時刻只允許一條線程對其進行l(wèi)ock操作,但lock操作可以被同一條線程重復執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖。lock和unlock必須成對出現(xiàn)
  • 如果對一個變量執(zhí)行l(wèi)ock操作,將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值
  • 如果一個變量事先沒有被lock操作鎖定,則不允許對它執(zhí)行unlock操作;也不允許去unlock一個被其他線程鎖定的變量。
  • 對一個變量執(zhí)行unlock操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)。

JMM下的可見性和有序性

可見性是指多線程環(huán)境下,一個線程對一個共享變量的修改能否及時地被其他線程觀察到。有序性是指有指令重排序?qū)е碌亩嗑€程數(shù)據(jù)安全問題。

解決可見性問題

  • Java中的volatile關(guān)鍵字:volatile關(guān)鍵字可以保證直接從主存中讀取一個變量,如果這個變量被修改后,總是會被寫回到主存中去。Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實現(xiàn)可見性的,無論是普通變量還是volatile變量都是如此,普通變量與volatile變量的區(qū)別是:volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每個線程在每次使用volatile變量前都立即從主內(nèi)存刷新。因此我們可以說volatile保證了多線程操作時變量的可見性,而普通變量則不能保證這一點。
  • Java中的synchronized關(guān)鍵字:同步快的可見性是由“如果對一個變量執(zhí)行l(wèi)ock操作,將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值”、“對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store和write操作)”這兩條規(guī)則獲得的。
    *Java中的final關(guān)鍵字:final關(guān)鍵字的可見性是指,被final修飾的字段在構(gòu)造器中一旦被初始化完成,并且構(gòu)造器沒有把“this”的引用傳遞出去(this引用逃逸是一件很危險的事情,其他線程有可能通過這個引用訪問到“初始化了一半”的對象),那么在其他線程就能看見final字段的值(無須同步)。

解決有序性問題

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

Java語言提供了volatile和synchronized兩個關(guān)鍵字來保證線程之間操作的有序性:

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

指令序列的重排序

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

2)指令級并行的重排序?,F(xiàn)代處理器采用了指令級并行技術(shù)(Instruction-LevelParallelism,ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應機器指令的執(zhí)行順序。

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

指令序列重排序

每個處理器上的寫緩沖區(qū),僅僅對它所在的處理器可見。這會導致處理器執(zhí)行內(nèi)存操作的順序可能會與內(nèi)存實際的操作執(zhí)行順序不一致。由于現(xiàn)代的處理器都會使用寫緩沖區(qū),因此現(xiàn)代的處理器都會允許對寫-讀操作進行重排序:

指令重排序規(guī)則

數(shù)據(jù)依賴

編譯器和處理器在重排序時,會遵守數(shù)據(jù)依賴性,編譯器和處理器不會改變存在數(shù)據(jù)依賴關(guān)系的兩個操作的執(zhí)行順序。(這里所說的數(shù)據(jù)依賴性僅針對單個處理器中執(zhí)行的指令序列和單個線程中執(zhí)行的操作,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮)

數(shù)據(jù)依賴

指令重排序?qū)?nèi)存可見性的影響

指令重排序?qū)?nèi)存可見性的影響

當1和2之間沒有數(shù)據(jù)依賴關(guān)系時,1和2之間就可能被重排序(3和4類似)。這樣的結(jié)果就是:讀線程B執(zhí)行4時,不一定能看到寫線程A在執(zhí)行1時對共享變量的修改。

as-if-serial語義

不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。(編譯器、runtime和處理器都必須遵守as-if-serial語義)

happens before

從JDK 5開始,Java使用新的JSR-133內(nèi)存模型,JSR-133使用happens-before的概念來闡述操作之間的內(nèi)存可見性:在JMM中,如果一個操作執(zhí)行的結(jié)果需要對另一個操作可見(兩個操作既可以是在一個線程之內(nèi),也可以是在不同線程之間),那么這兩個操作之間必須要存在happens-before關(guān)系:

程序順序規(guī)則:一個線程中的每個操作,happens-before于該線程中的任意后續(xù)操作。
監(jiān)視器鎖規(guī)則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖。
volatile變量規(guī)則:對一個volatile域的寫,happens-before于任意后續(xù)對這個volatile域的讀。
傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
一個happens-before規(guī)則對應于一個或多個編譯器和處理器重排序規(guī)則。

內(nèi)存屏障禁止特定類型的處理器重排序

重排序可能會導致多線程程序出現(xiàn)內(nèi)存可見性問題。對于處理器重排序,JMM的處理器重排序規(guī)則會要求Java編譯器在生成指令序列時,插入特定類型的內(nèi)存屏障(Memory Barriers,Intel稱之為Memory Fence)指令,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序。通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見性保證。

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

內(nèi)存屏障

StoreLoad Barriers是一個“全能型”的屏障,它同時具有其他3個屏障的效果?,F(xiàn)代的多處理器大多支持該屏障(其他類型的屏障不一定被所有處理器支持)。執(zhí)行該屏障開銷會很昂貴,因為當前處理器通常要把寫緩沖區(qū)中的數(shù)據(jù)全部刷新到內(nèi)存中(Buffer Fully Flush)。

參考資料

《Java并發(fā)編程的藝術(shù)》

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

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

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