Java內存模型分析


在學習Java內存模型以前,先了解一下線程通訊機制。html

一、線程通訊機制

在併發編程中,線程之間相互交換信息就是線程通訊。目前有兩種機制:內存共享與消息傳遞。java

1.一、共享內存

Java採用的就是共享內存,本次學習的主要內容就是這個內存模型。 內存共享方式必須經過鎖或者CAS技術來獲取或者修改共享的變量,看起來比較簡單,可是鎖的使用難度比較大,業務複雜的話還有可能發生死鎖。git

1.二、消息傳遞

Actor模型便是一個異步的、非阻塞的消息傳遞機制。Akka是對於Java的Actor模型庫,用於構建高併發、分佈式、可容錯、事件驅動的基於JVM的應用。 消息傳遞方式就是顯示的經過發送消息來進行線程間通訊,對於大型複雜的系統,可能優點更足。github

1.三、共享內存與消息傳遞的區別

線程通訊機制區別.png

二、內存模型

Java既然使用內存共享,必然就涉及到內存模型。web

2.一、結構抽象

內存模型結構的抽象分爲兩個層次:編程

  • 多核CPU與內存之間
  • Java多線程與主存之間
2.1.一、多核CPU與內存之間

由於CPU的運行速度與內存之間的存取速度不成正比,因此,引入了多級緩存概念,相應的也引出了緩存讀取不一致問題,固然緩存一致性協議解決了這個問題(本文不深刻討論)。 結構抽象如圖:segmentfault

多核處理器和內存交互.jpg

2.1.二、Java多線程與主存之間

JMM規定了全部的變量都存儲在主內存(Main Memory)中。緩存

每一個線程有本身的工做內存(Working Memory),線程的工做內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量(volatile變量仍然有工做內存的拷貝,可是因爲它特殊的操做順序性規定,因此看起來如同直接在主內存中讀寫訪問通常)。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程之間值的傳遞都須要經過主內存來完成。 如圖: JAVA內存模型抽象.jpg多線程

JAVA內存模型抽象示意圖.png

2.二、重排序

重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行從新排序的一種手段。 例如,若是一個線程更新字段 A的值,而後更新字段B的值,並且字段B 的值不依賴於字段A 的值,那麼,處理器就可以自由的調整它們的執行順序,並且緩衝區可以在更新字段 A以前更新字段 B的值到主內存。架構

2.2.一、數據依賴

若是兩個操做訪問同一個變量,且這兩個操做中有一個爲寫操做,此時這兩個操做之間就存在數據依賴性。

數據依賴.png

如圖所示,A和C之間存在數據依賴關係,同時B和C之間也存在數據依賴關係。所以在最終執行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的結果將會被改變)。但A和B之間沒有數據依賴關係,編譯器和處理器能夠重排序A和B之間的執行順序。

2.2.二、as-if-serial語義

as-if-serial語義的意思是,全部的操做都可覺得了優化而被重排序,可是你必需要保證重排序後執行的結果不能被改變,編譯器、runtime、處理器都必須遵照as-if-serial語義。注意as-if-serial只保證單線程環境,多線程環境下無效。

as-if-serial語義使得重排序不會干擾單線程程序,也無需擔憂內存可見性問題。

2.2.三、重排序類型
  1. 編譯器優化的重排序 編譯器在不改變單線程程序語義的前提下,能夠從新安排語義。
  2. 指令級並行的重排序 代處理器採用了指令級並行技術(Instruction-Level Parallelism,ILP)來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。
  3. 內存系統的重排序 因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。

從Java源代碼到最終實際執行的指令序列,會分別經歷下面3種重排序。

reorder.jpg

2.2.四、禁止重排序
  • 只要volatile變量與普通變量之間的重排序可能會破壞volatile的內存語義,這種重排序就會被編譯器排序規則和處理器內存屏障插入策略禁止。
  • Java內存模型的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障(Memory Barriers,Intel稱之爲Memory Fence)指令,經過內存屏障指令來禁止特定類型的處理器重排序。
  • 在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。(防止拿到對象時,final域還未賦值);初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操做之間不能重排序。
2.2.五、重排序對多線程的影響

重排序不會影響單線程環境的執行結果,可是會破壞多線程的執行語義。

2.三、順序一致性

順序一致性是多線程環境下的理論參考模型,爲程序提供了極強的內存可見性保證,在順序一致性執行過程當中,全部動做之間的前後關係與程序代碼的順序一致。

JMM對正確同步的多線程程序的內存一致性作出的保證: 若是程序是正確同步的,程序的執行將具備順序一致性(sequentially consistent)。

2.3.一、特性
  • 一個線程中的全部操做一定按照程序的順序來執行。
  • 全部的線程都只能看到一個單一的執行順序,不論是否同步。
  • 每一個操做都必須原子執行且當即對全部程序可見。
2.3.二、例子
  • 加了鎖 順序一致性-加鎖.png
  • 未加鎖 順序一致性-未加鎖.png

2.四、多線程內存可見性-happens before

在併發編程時,會碰到一個難題:即一個操做A的結果對另外一個操做B可見,即多線程變量可見性問題。 解決方法就是提出了happens-before概念,即一個操做A與另外一個操做B存在happens-before關係。

2.4.一、定義

《Time,Clocks and the Ordering of Events in a Distributed System》點擊查看論文。

  • 若是一個操做happens-before另外一個操做,那麼第一個操做的執行結果將對第二個操做可見,並且第一個操做的執行順序排在第二個操做以前。
  • 兩個操做之間存在happens-before關係,並不意味着必定要按照happens-before原則制定的順序來執行。若是重排序以後的執行結果與按照happens-before關係來執行的結果一致,那麼這種重排序並不非法。

前提:操做A happens-before 操做B。 對於第一條,編碼時,A操做在B操做以前,則執行順序就是A以後B。 對於第二條,若是重排序後,雖然執行順序不是A到B,可是最終A的結果對B可見,則容許這種重排序。

2.4.二、規則
  1. 程序次序規則: 一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做,這個規則只對單線程有效,在多線程環境下沒法保證正確性。
  2. 鎖定規則: 無論單線程多線程,一個unLock操做先行發生於後面對同一個鎖的lock操做。
  3. volatile變量規則: 它標誌着volatile保證了線程可見性。通俗點講就是若是一個線程先去寫一個volatile變量,而後另外一個線程去讀這個變量,那麼這個寫操做必定是happens-before讀操做的。
  4. 傳遞規則: 若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於操做C;
  5. 線程啓動規則: 假定線程A在執行過程當中,經過執行ThreadB.start()來啓動線程B,那麼線程A對共享變量的修改在接下來線程B開始執行後確保對線程B可見。即:調用start方法時,會將start方法以前全部操做的結果同步到主內存中,新線程建立好後,須要從主內存獲取數據。這樣在start方法調用以前的全部操做結果對於新建立的線程都是可見的。
  6. 線程中斷規則: 對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。
  7. 線程終結規則: 線程中全部的操做都先行發生於線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行;
  8. 對象終結規則: 一個對象的初始化完成先行發生於他的finalize()方法的開始;
2.4.三、Happens-Before原則究竟是如何解決變量間可見性問題的?

重排序CPU高速緩存有利於計算機性能的提升,但卻對多CPU處理的一致性帶來了影響。爲了解決這個矛盾,咱們能夠採起一種折中的辦法。咱們用分割線把整個程序劃分紅幾個程序塊,在每一個程序塊內部的指令是能夠重排序的,可是分割線上的指令與程序塊的其它指令之間是不能夠重排序的。在一個程序塊內部,CPU不用每次都與主內存進行交互,只須要在CPU緩存中執行讀寫操做便可,可是當程序執行到分割線處,CPU必須將執行結果同步到主內存或從主內存讀取最新的變量值。那麼,Happens-Before規則就是定義了這些程序塊的分割線。下圖展現了一個使用鎖定原則做爲分割線的例子: happens-before.jpg

如圖所示,這裏的unlock M和lock M就是劃分程序的分割線。在這裏,紅色區域和綠色區域的代碼內部是能夠進行重排序的,可是unlock和lock操做是不能與它們進行重排序的。即第一個圖中的紅色部分必需要在unlock M指令以前所有執行完,第二個圖中的綠色部分必須所有在lock M指令以後執行。而且在第一個圖中的unlock M指令處,紅色部分的執行結果要所有刷新到主存中,在第二個圖中的lock M指令處,綠色部分用到的變量都要從主存中從新讀取。 在程序中加入分割線將其劃分紅多個程序塊,雖然在程序塊內部代碼仍然可能被重排序,可是保證了程序代碼在宏觀上是有序的。而且能夠確保在分割線處,CPU必定會和主內存進行交互。Happens-Before原則就是定義了程序中什麼樣的代碼能夠做爲分隔線。而且不管是哪條Happens-Before原則,它們所產生分割線的做用都是相同的。

2.五、內存屏障

內存屏障是爲了解決在cacheline上的操做重排序問題。

2.5.一、做用

強制CPU將store buffer中的內容寫入到 cacheline中。 強制CPU將invalidate queue中的請求處理完畢。

2.5.二、類型
屏障類型 指令示例 說明
LoadLoad Barriers Load1;LoadLoad;Load2 該屏障確保Load1數據的裝載先於Load2及其後全部裝載指令的的操做
StoreStore Barriers Store1;StoreStore;Store2 該屏障確保Store1馬上刷新數據到內存(使其對其餘處理器可見)的操做先於Store2及其後全部存儲指令的操做
LoadStore Barriers Load1;LoadStore;Store2 確保Load1的數據裝載先於Store2及其後全部的存儲指令刷新數據到內存的操做
StoreLoad Barriers Store1;StoreLoad;Load1 該屏障確保Store1馬上刷新數據到內存的操做先於Load2及其後全部裝載裝載指令的操做.它會使該屏障以前的全部內存訪問指令(存儲指令和訪問指令)完成以後,才執行該屏障以後的內存訪問指令

StoreLoad Barriers同時具有其餘三個屏障的效果,所以也稱之爲全能屏障,是目前大多數處理器所支持的,可是相對其餘屏障,該屏障的開銷相對昂貴.在x86架構的處理器的指令集中,lock指令能夠觸發StoreLoad Barriers.

2.5.三、內存屏障在Java中的體現
2.5.3.一、volatile
  • volatile讀以後,全部變量讀寫操做都不會重排序到其前面。
  • volatile讀以前,全部volatile讀寫操做都已完成。
  • volatile寫以後,volatile變量讀寫操做都不會重排序到其前面。
  • volatile寫以前,全部變量的讀寫操做都已完成。

根據JMM規則,結合內存屏障的相關分析:

  • 在每個volatile寫操做前面插入一個StoreStore屏障。這確保了在進行volatile寫以前前面的全部普通的寫操做都已經刷新到了內存。
  • 在每個volatile寫操做後面插入一個StoreLoad屏障。這樣能夠避免volatile寫操做與後面可能存在的volatile讀寫操做發生重排序。
  • 在每個volatile讀操做後面插入一個LoadLoad屏障。這樣能夠避免volatile讀操做和後面普通的讀操做進行重排序。
  • 在每個volatile讀操做後面插入一個LoadStore屏障。這樣能夠避免volatile讀操做和後面普通的寫操做進行重排序。
2.5.3.二、final:
  • 寫 final 域的重排序規則 JMM 禁止編譯器把 final 域的寫重排序到構造函數以外。 編譯器會在 final 域的寫以後,構造函數 return 以前,插入一個 StoreStore 屏障。這個屏障禁止處理器把 final 域的寫重排序到構造函數以外。
  • 讀 final 域的重排序規則 在一個線程中,初次讀對象引用與初次讀該對象包含的 final 域,JMM 禁止處理器重排序這兩個操做(注意,這個規則僅僅針對處理器)。編譯器會在讀 final 域操做的前面插入一個 LoadLoad 屏障。
2.5.3.三、CAS

在CPU架構中依靠lock信號保證可見性並禁止重排序。 lock前綴是一個特殊的信號,執行過程以下:

  • 對總線和緩存上鎖。
  • 強制全部lock信號以前的指令,都在此以前被執行,並同步相關緩存。
  • 執行lock後的指令(如cmpxchg)。
  • 釋放對總線和緩存上的鎖。
  • 強制全部lock信號以後的指令,都在此以後被執行,並同步相關緩存。

所以,lock信號雖然不是內存屏障,但具備mfence的語義(固然,還有排他性的語義)。 與內存屏障相比,lock信號要額外對總線和緩存上鎖,成本更高。

2.5.3.四、鎖

JVM的內置鎖經過操做系統的管程實現。因爲管程是一種互斥資源,修改互斥資源至少須要一個CAS操做。所以,鎖必然也使用了lock信號,具備mfence的語義。

參考

《Java併發編程的藝術》一一3.2 重排序 啃碎併發(11):內存模型之重排序 【細談Java併發】內存模型之重排序 【死磕Java併發】-----Java內存模型之重排序 http://www.javashuo.com/article/p-ooicwjrf-cg.html http://www.javashuo.com/article/p-gyfgocgd-hy.html http://www.javashuo.com/article/p-aaxpbbwh-mv.html 一文解決內存屏障 內存屏障與 JVM 併發 內存屏障和 volatile 語義 Java內存模型Cookbook(二)內存屏障 談亂序執行和內存屏障 內存屏障 深刻理解 Java 內存模型(六)——final 僞共享(FalseSharing) 避免並發現線程之間的假共享 僞共享(FalseSharing)和緩存行(CacheLine)大雜燴 僞共享(falsesharing),併發編程無聲的性能殺手 Java8使用@sun.misc.Contended避免僞共享

tencent.jpg

相關文章
相關標籤/搜索