java内存模型之一
1.1并發(fā)編程模型的兩個(gè)關(guān)鍵問題:
線程之間的通信機(jī)制有兩種:共享內(nèi)存和消息傳遞,在共享內(nèi)存的并發(fā)模型里,線程之間共享程序的公共狀態(tài),通過寫-讀內(nèi)存中的公共狀態(tài)進(jìn)行隱式通信。在消息傳遞的并發(fā)模型里,線程之間沒有公共狀態(tài),線程之間必須通過發(fā)送消息來顯式進(jìn)行通信。
1.2Java內(nèi)存模型的抽象結(jié)構(gòu):
在Java中,所有實(shí)例域、靜態(tài)域和數(shù)組元素都存儲(chǔ)在堆內(nèi)存中,堆內(nèi)存在線程之間共享 ,局部變量(Local Variables),方 法定義參數(shù)(Java語言規(guī)范稱之為Formal Method Parameters)和異常處理器參數(shù)(Exception Handler Parameters)不會(huì)在線程之間共享,它們不會(huì)有內(nèi)存可見性問題,也不受內(nèi)存模型的影響。
Java線程之間的通信由Java內(nèi)存模型(本文簡(jiǎn)稱為JMM)控制,JMM決定一個(gè)線程對(duì)共享 變量的寫入何時(shí)對(duì)另一個(gè)線程可見。從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽 象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存(Main Memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲(chǔ)了該線程以讀/寫共享變量的副本。本地內(nèi)存是JMM的
一個(gè)抽象概念,并不真實(shí)存在。它涵蓋了緩存、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu) 化。Java內(nèi)存模型的抽象示意如圖3-1所示。
從圖上圖來看,如果線程A與線程B之間要通信的話,必須要經(jīng)歷下面2個(gè)步驟。
1)線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去。
2)線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量。
下面通過示意圖來說明這兩個(gè)步驟。
從整體來看,這兩個(gè)步驟實(shí)質(zhì)上是線程A在向線程B發(fā)送消息,而且這個(gè)通信過程必須要 經(jīng)過主內(nèi)存。JMM通過控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互,來為Java程序員提供
內(nèi)存可見性保證。
1.3?源代碼到指令重排序
在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序。重排序分3種類型。
1)編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。
2)指令級(jí)并行的重排序?,F(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction-Level Parallelism,ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對(duì)機(jī)器指令的執(zhí)行順序。
3)內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。
?從Java源代碼到最終實(shí)際執(zhí)行的指令序列,會(huì)分別經(jīng)歷下面3種重排序,如下所示
上述的1屬于編譯器重排序,2和3屬于處理器重排序。這些重排序可能會(huì)導(dǎo)致多線程程序 出現(xiàn)內(nèi)存可見性問題。對(duì)于編譯器,JMM的編譯器重排序規(guī)則會(huì)禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。對(duì)于處理器重排序,JMM的處理器重排序規(guī)則會(huì)要求Java編譯器在生成指令序列時(shí),插入特定類型的內(nèi)存屏障(Memory Barriers,Intel稱之為 Memory Fence)指令,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序。
1.3?并發(fā)編程模型分類
每個(gè)處理器上的寫緩沖區(qū),僅僅對(duì)它所在的處理器 可見。這個(gè)特性會(huì)對(duì)內(nèi)存操作的執(zhí)行順序產(chǎn)生重要的影響:處理器對(duì)內(nèi)存的讀/寫操作的執(zhí)行 順序,不一定與內(nèi)存實(shí)際發(fā)生的讀/寫操作順序一致!
為了保證內(nèi)存可見性,Java編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來禁 止特定類型的處理器重排序。JMM把內(nèi)存屏障指令分為4類:
?
StoreLoad Barriers是一個(gè)“全能型”的屏障,它同時(shí)具有其他3個(gè)屏障的效果。現(xiàn)代的多處理器大多支持該屏障(其他類型的屏障不一定被所有處理器支持)。執(zhí)行該屏障開銷會(huì)很昂 貴,因?yàn)楫?dāng)前處理器通常要把寫緩沖區(qū)中的數(shù)據(jù)全部刷新到內(nèi)存中(Buffer Fully Flush)。
1.4happen-before
Java使用新的JSR-133內(nèi)存模型,JSR-133使用happens-before的概念來闡述操作之間的內(nèi)存可見性。在JMM中,如果一 個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見,那么這兩個(gè)操作之間必須要存在happens-before關(guān)系。這里提到的兩個(gè)操作既可以是在一個(gè)線程之內(nèi),也可以是在不同線程之間。
與程序員密切相關(guān)的happens-before規(guī)則如下:
程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before于該線程中的任意后續(xù)操作。
監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖,happens-before于隨后對(duì)這個(gè)鎖的加鎖
? ??volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀
傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C
注意 兩個(gè)操作之間具有happens-before關(guān)系,并不意味著前一個(gè)操作必須要在后一個(gè) 操作之前執(zhí)行!happens-before僅僅要求前一個(gè)操作(執(zhí)行的結(jié)果)對(duì)后一個(gè)操作可見,且前一 個(gè)操作按順序排在第二個(gè)操作之前(the first is visible to and ordered before the second)。 happens-before的定義很微妙,后文會(huì)具體說明happens-before為什么要這么定義。
1.5?重排序
1.5.1?數(shù)據(jù)依賴性
如果兩個(gè)操作訪問同一個(gè)變量,且這兩個(gè)操作中有一個(gè)為寫操作,此時(shí)這兩個(gè)操作之間 就存在數(shù)據(jù)依賴性。數(shù)據(jù)依賴分為下列3種類型,如表3-4所示。 前面提到過,編譯器和處理器可能會(huì)對(duì)操作做重排序。編譯器和處理器在重排序時(shí),會(huì)遵守?cái)?shù)據(jù)依賴性,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序。
這里所說的數(shù)據(jù)依賴性僅針對(duì)單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮。
? ? ? ?1.5.2as-if-serial語義
as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程) 程序的執(zhí)行結(jié)果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義;
? ? ? 控制依賴性:
當(dāng)代碼中存在控制依賴性時(shí),會(huì)影響指令序 列執(zhí)行的并行度。為此,編譯器和處理器會(huì)采用猜測(cè)(Speculation)執(zhí)行來克服控制相關(guān)性對(duì)并 行度的影響。以處理器的猜測(cè)執(zhí)行為例,執(zhí)行線程B的處理器可以提前讀取并計(jì)算a*a,然后把 計(jì)算結(jié)果臨時(shí)保存到一個(gè)名為重排序緩沖(Reorder Buffer,ROB)的硬件緩存中。當(dāng)操作3的條 件判斷為真時(shí),就把該計(jì)算結(jié)果寫入變量i中。
1.6?順序一致性
1.6.1順序一致性內(nèi)存模型 順序一致性內(nèi)存模型是一個(gè)被計(jì)算機(jī)科學(xué)家理想化了的理論參考模型,它為程序員提供了極強(qiáng)的內(nèi)存可見性保證。順序一致性內(nèi)存模型有兩大特性
? 1)一個(gè)線程中的所有操作必須按照程序的順序來執(zhí)行?! ?/p>
? ? 2)(不管程序是否同步)所有線程都只能看到一個(gè)單一的操作執(zhí)行順序。在順序一致性內(nèi)存模型中,每個(gè)操作都必須原子執(zhí)行且立刻對(duì)所有線程可見。
? ??
在概念上,順序一致性模型有一個(gè)單一的全局內(nèi)存,這個(gè)內(nèi)存通過一個(gè)左右擺動(dòng)的開關(guān) 可以連接到任意一個(gè)線程,同時(shí)每一個(gè)線程必須按照程序的順序來執(zhí)行內(nèi)存讀/寫操作。從上
面的示意圖可以看出,在任意時(shí)間點(diǎn)最多只能有一個(gè)線程可以連接到內(nèi)存。當(dāng)多個(gè)線程并發(fā) 執(zhí)行時(shí),圖中的開關(guān)裝置能把所有線程的所有內(nèi)存讀/寫操作串行化(即在順序一致性模型中,
所有操作之間具有全序關(guān)系)。
樣例理解:
假設(shè)有兩個(gè)線程A和B并發(fā)執(zhí)行。其中A線程有3個(gè)操作,它們?cè)诔绦蛑械捻樞蚴?#xff1a; A1→A2→A3。B線程也有3個(gè)操作,它們?cè)诔绦蛑械捻樞蚴?#xff1a;B1→B2→B3。假設(shè)這兩個(gè)線程使用監(jiān)視器鎖來正確同步:A線程的3個(gè)操作執(zhí)行后釋放監(jiān)視器鎖,隨后B 線程獲取同一個(gè)監(jiān)視器鎖。那么程序在順序一致性模型中的執(zhí)行效果將如圖3-11所示。
?
現(xiàn)在我們?cè)偌僭O(shè)這兩個(gè)線程沒有做同步,下面是這個(gè)未同步程序在順序一致性模型中的執(zhí)行示意圖,如圖3-12所示。
?1.6.2?同步程序的順序一致性
?1.6.3 未同步程序的順序一致性
對(duì)于未同步或未正確同步的多線程程序,JMM只提供最小安全性:線程執(zhí)行時(shí)讀取到的 值,要么是之前某個(gè)線程寫入的值,要么是默認(rèn)值(0,Null,False),JMM保證線程讀操作讀取 到的值不會(huì)無中生有(Out Of Thin Air)的冒出來。為了實(shí)現(xiàn)最小安全性,JVM在堆上分配對(duì)象 時(shí),首先會(huì)對(duì)內(nèi)存空間進(jìn)行清零,然后才會(huì)在上面分配對(duì)象(JVM內(nèi)部會(huì)同步這兩個(gè)操作)。因 此,在已清零的內(nèi)存空間(Pre-zeroed Memory)分配對(duì)象時(shí),域的默認(rèn)初始化已經(jīng)完成了?!?/p>
未同步程序在JMM中的執(zhí)行時(shí),整體上是無序的,其執(zhí)行結(jié)果無法預(yù)知。未同步程序在兩個(gè)模型中的執(zhí)行特性有如下幾個(gè)差異:
1?)順序一致性模型保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行,而JMM不保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行(比如上面正確同步的多線程程序在臨界區(qū)內(nèi)的重排序)。
2)順序一致性模型保證所有線程只能看到一致的操作執(zhí)行順序,而JMM不保證所有線程能看到一致的操作執(zhí)行順序。
3)JMM不保證對(duì)64位的long型和double型變量的寫操作具有原子性,而順序一致性模型保 證對(duì)所有的內(nèi)存讀/寫操作都具有原子性。
第3個(gè)差異與處理器總線的工作機(jī)制密切相關(guān)。在計(jì)算機(jī)中,數(shù)據(jù)通過總線在處理器和內(nèi)
存之間傳遞。每次處理器和內(nèi)存之間的數(shù)據(jù)傳遞都是通過一系列步驟來完成的,這一系列步
驟稱之為總線事務(wù)(Bus Transaction)。總線事務(wù)包括讀事務(wù)(Read Transaction)和寫事務(wù)(Write Transaction)。讀事務(wù)從內(nèi)存?zhèn)魉蛿?shù)據(jù)到處理器,寫事務(wù)從處理器傳送數(shù)據(jù)到內(nèi)存,每個(gè)事務(wù)會(huì) 讀/寫內(nèi)存中一個(gè)或多個(gè)物理上連續(xù)的字。這里的關(guān)鍵是,總線會(huì)同步試圖并發(fā)使用總線的事 務(wù)。在一個(gè)處理器執(zhí)行總線事務(wù)期間,總線會(huì)禁止其他的處理器和I/O設(shè)備執(zhí)行內(nèi)存的讀/寫。 下面,讓我們通過一個(gè)示意圖來說明總線的工作機(jī)制,如
由圖可知,假設(shè)處理器A,B和C同時(shí)向總線發(fā)起總線事務(wù),這時(shí)總線仲裁(Bus Arbitration) 會(huì)對(duì)競(jìng)爭(zhēng)做出裁決,這里假設(shè)總線在仲裁后判定處理器A在競(jìng)爭(zhēng)中獲勝(總線仲裁會(huì)確保所有 處理器都能公平的訪問內(nèi)存)。此時(shí)處理器A繼續(xù)它的總線事務(wù),而其他兩個(gè)處理器則要等待 處理器A的總線事務(wù)完成后才能再次執(zhí)行內(nèi)存訪問。假設(shè)在處理器A執(zhí)行總線事務(wù)期間(不管 這個(gè)總線事務(wù)是讀事務(wù)還是寫事務(wù)),處理器D向總線發(fā)起了總線事務(wù),此時(shí)處理器D的請(qǐng)求會(huì)被總線禁止。
總線的這些工作機(jī)制可以把所有處理器對(duì)內(nèi)存的訪問以串行化的方式來執(zhí)行。在任意時(shí) 間點(diǎn),最多只能有一個(gè)處理器可以訪問內(nèi)存。這個(gè)特性確保了單個(gè)總線事務(wù)之中的內(nèi)存讀/寫
操作具有原子性。
在一些32位的處理器上,如果要求對(duì)64位數(shù)據(jù)的寫操作具有原子性,會(huì)有比較大的開銷。 為了照顧這種處理器,Java語言規(guī)范鼓勵(lì)但不強(qiáng)求JVM對(duì)64位的long型變量和double型變量的 寫操作具有原子性。當(dāng)JVM在這種處理器上運(yùn)行時(shí),可能會(huì)把一個(gè)64位long/double型變量的寫操作拆分為兩個(gè)32位的寫操作來執(zhí)行。這兩個(gè)32位的寫操作可能會(huì)被分配到不同的總線事務(wù)中執(zhí)行,此時(shí)對(duì)這個(gè)64位變量的寫操作將不具有原子性。
如上圖所示,假設(shè)處理器A寫一個(gè)long型變量,同時(shí)處理器B要讀這個(gè)long型變量。處理器 A中64位的寫操作被拆分為兩個(gè)32位的寫操作,且這兩個(gè)32位的寫操作被分配到不同的寫事 務(wù)中執(zhí)行。同時(shí),處理器B中64位的讀操作被分配到單個(gè)的讀事務(wù)中執(zhí)行。當(dāng)處理器A和B按上 圖的時(shí)序來執(zhí)行時(shí),處理器B將看到僅僅被處理器A“寫了一半”的無效值?!?/p>
注意,在JSR-133之前的舊內(nèi)存模型中,一個(gè)64位long/double型變量的讀/寫操作可以被拆 分為兩個(gè)32位的讀/寫操作來執(zhí)行。從JSR-133內(nèi)存模型開始(即從JDK5開始),僅僅只允許把 一個(gè)64位long/double型變量的寫操作拆分為兩個(gè)32位的寫操作來執(zhí)行,任意的讀操作在JSR133中都必須具有原子性(即任意讀操作必須要在單個(gè)讀事務(wù)中執(zhí)行)。
轉(zhuǎn)載于:https://www.cnblogs.com/sharing-java/p/10825115.html
總結(jié)
以上是生活随笔為你收集整理的java内存模型之一的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: bzoj5368 [Pkusc2018]
- 下一篇: 电气图图形符号