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