在學習Java內存模型以前,先了解一下線程通訊機制。html
在併發編程中,線程之間相互交換信息就是線程通訊。目前有兩種機制:內存共享與消息傳遞。java
Java採用的就是共享內存,本次學習的主要內容就是這個內存模型。
內存共享方式必須經過鎖或者CAS技術來獲取或者修改共享的變量,看起來比較簡單,可是鎖的使用難度比較大,業務複雜的話還有可能發生死鎖。git
Actor模型便是一個異步的、非阻塞的消息傳遞機制。Akka是對於Java的Actor模型庫,用於構建高併發、分佈式、可容錯、事件驅動的基於JVM的應用。
消息傳遞方式就是顯示的經過發送消息來進行線程間通訊,對於大型複雜的系統,可能優點更足。github
Java既然使用內存共享,必然就涉及到內存模型。編程
內存模型結構的抽象分爲兩個層次:segmentfault
由於CPU的運行速度與內存之間的存取速度不成正比,因此,引入了多級緩存概念,相應的也引出了緩存讀取不一致問題,固然緩存一致性協議解決了這個問題(本文不深刻討論)。
結構抽象如圖:緩存
JMM規定了全部的變量都存儲在主內存(Main Memory)中。多線程
每一個線程有本身的工做內存(Working Memory),線程的工做內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量(volatile變量仍然有工做內存的拷貝,可是因爲它特殊的操做順序性規定,因此看起來如同直接在主內存中讀寫訪問通常)。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程之間值的傳遞都須要經過主內存來完成。
如圖:架構
重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行從新排序的一種手段。
例如,若是一個線程更新字段 A的值,而後更新字段B的值,並且字段B 的值不依賴於字段A 的值,那麼,處理器就可以自由的調整它們的執行順序,並且緩衝區可以在更新字段 A以前更新字段 B的值到主內存。併發
若是兩個操做訪問同一個變量,且這兩個操做中有一個爲寫操做,此時這兩個操做之間就存在數據依賴性。
如圖所示,A和C之間存在數據依賴關係,同時B和C之間也存在數據依賴關係。所以在最終執行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的結果將會被改變)。但A和B之間沒有數據依賴關係,編譯器和處理器能夠重排序A和B之間的執行順序。
as-if-serial語義的意思是,全部的操做都可覺得了優化而被重排序,可是你必需要保證重排序後執行的結果不能被改變,編譯器、runtime、處理器都必須遵照as-if-serial語義。注意as-if-serial只保證單線程環境,多線程環境下無效。
as-if-serial語義使得重排序不會干擾單線程程序,也無需擔憂內存可見性問題。
編譯器在不改變單線程程序語義的前提下,能夠從新安排語義。
代處理器採用了指令級並行技術(Instruction-Level Parallelism,ILP)來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。
因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。
從Java源代碼到最終實際執行的指令序列,會分別經歷下面3種重排序。
重排序不會影響單線程環境的執行結果,可是會破壞多線程的執行語義。
順序一致性是多線程環境下的理論參考模型,爲程序提供了極強的內存可見性保證,在順序一致性執行過程當中,全部動做之間的前後關係與程序代碼的順序一致。
JMM對正確同步的多線程程序的內存一致性作出的保證:
若是程序是正確同步的,程序的執行將具備順序一致性(sequentially consistent)。
在併發編程時,會碰到一個難題:即一個操做A的結果對另外一個操做B可見,即多線程變量可見性問題。
解決方法就是提出了happens-before概念,即一個操做A與另外一個操做B存在happens-before關係。
《Time,Clocks and the Ordering of Events in a Distributed System》
)點擊查看論文。
前提:操做A happens-before 操做B。
對於第一條,編碼時,A操做在B操做以前,則執行順序就是A以後B。
對於第二條,若是重排序後,雖然執行順序不是A到B,可是最終A的結果對B可見,則容許這種重排序。
一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做,這個規則只對單線程有效,在多線程環境下沒法保證正確性。
無論單線程多線程,一個unLock操做先行發生於後面對同一個鎖的lock操做。
它標誌着volatile保證了線程可見性。通俗點講就是若是一個線程先去寫一個volatile變量,而後另外一個線程去讀這個變量,那麼這個寫操做必定是happens-before讀操做的。
若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於操做C;
假定線程A在執行過程當中,經過執行ThreadB.start()來啓動線程B,那麼線程A對共享變量的修改在接下來線程B開始執行後確保對線程B可見。即:調用start方法時,會將start方法以前全部操做的結果同步到主內存中,新線程建立好後,須要從主內存獲取數據。這樣在start方法調用以前的全部操做結果對於新建立的線程都是可見的。
對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。
線程中全部的操做都先行發生於線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行;
一個對象的初始化完成先行發生於他的finalize()方法的開始;
重排序
和CPU高速緩存
有利於計算機性能的提升,但卻對多CPU處理的一致性帶來了影響。爲了解決這個矛盾,咱們能夠採起一種折中的辦法。咱們用分割線把整個程序劃分紅幾個程序塊,在每一個程序塊內部的指令是能夠重排序的,可是分割線上的指令與程序塊的其它指令之間是不能夠重排序的。在一個程序塊內部,CPU不用每次都與主內存進行交互,只須要在CPU緩存中執行讀寫操做便可,可是當程序執行到分割線處,CPU必須將執行結果同步到主內存或從主內存讀取最新的變量值。那麼,Happens-Before規則就是定義了這些程序塊的分割線。下圖展現了一個使用鎖定原則做爲分割線的例子:
如圖所示,這裏的unlock M和lock M就是劃分程序的分割線。在這裏,紅色區域和綠色區域的代碼內部是能夠進行重排序的,可是unlock和lock操做是不能與它們進行重排序的。即第一個圖中的紅色部分必需要在unlock M指令以前所有執行完,第二個圖中的綠色部分必須所有在lock M指令以後執行。而且在第一個圖中的unlock M指令處,紅色部分的執行結果要所有刷新到主存中,在第二個圖中的lock M指令處,綠色部分用到的變量都要從主存中從新讀取。
在程序中加入分割線將其劃分紅多個程序塊,雖然在程序塊內部代碼仍然可能被重排序,可是保證了程序代碼在宏觀上是有序的。而且能夠確保在分割線處,CPU必定會和主內存進行交互。Happens-Before原則就是定義了程序中什麼樣的代碼能夠做爲分隔線。而且不管是哪條Happens-Before原則,它們所產生分割線的做用都是相同的。
內存屏障是爲了解決在cacheline上的操做重排序問題。
強制CPU將store buffer中的內容寫入到 cacheline中。
強制CPU將invalidate queue中的請求處理完畢。
屏障類型 | 指令示例 | 說明 |
---|---|---|
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.
根據JMM規則,結合內存屏障的相關分析:
JMM 禁止編譯器把 final 域的寫重排序到構造函數以外。
編譯器會在 final 域的寫以後,構造函數 return 以前,插入一個 StoreStore 屏障。這個屏障禁止處理器把 final 域的寫重排序到構造函數以外。
在一個線程中,初次讀對象引用與初次讀該對象包含的 final 域,JMM 禁止處理器重排序這兩個操做(注意,這個規則僅僅針對處理器)。編譯器會在讀 final 域操做的前面插入一個 LoadLoad 屏障。
在CPU架構中依靠lock信號保證可見性並禁止重排序。
lock前綴是一個特殊的信號,執行過程以下:
所以,lock信號雖然不是內存屏障,但具備mfence的語義(固然,還有排他性的語義)。
與內存屏障相比,lock信號要額外對總線和緩存上鎖,成本更高。
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避免僞共享