來源:codeceo
http://www.codeceo.com/javamemorymodel.htmlhtml
JMM簡介java
Java Memory Model簡稱JMM, 是一系列的Java虛擬機平臺對開發者提供的多線程環境下的內存可見性、是否能夠重排序等問題的無關具體平臺的統一的保證。(可能在術語上與Java運行時內存分佈有歧義,後者指堆、方法區、線程棧等內存區域)。spring
併發編程有多種風格,除了CSP(通訊順序進程)、Actor等模型外,你們最熟悉的應該是基於線程和鎖的共享內存模型了。在多線程編程中,須要注意三類併發問題:編程
原子性數組
可見性緩存
重排序安全
原子性涉及到,一個線程執行一個複合操做的時候,其餘線程是否可以看到中間的狀態、或進行干擾。典型的就是i++的問題了,兩個線程同時對共享的堆內存執行++操做,而++操做在JVM、運行時、CPU中的實現均可能是一個複合操做, 例如在JVM指令的角度來看是將i的值從堆內存讀到操做數棧、加上1、再寫回到堆內存的i,這幾個操做的期間,若是沒有正確的同步,其餘線程也能夠同時執行,可能致使數據丟失等問題。常見的原子性問題又叫競太條件,是基於一個可能失效的結果進行判斷,如讀取-修改-寫入。 可見性和重排序問題都源於系統的優化。多線程
因爲CPU的執行速度和內存的存取速度嚴重不匹配,爲了優化性能,基於時間局部性、空間局部性等局部性原理,CPU在和內存間增長了多層高速緩存,當須要取數據時,CPU會先到高速緩存中查找對應的緩存是否存在,存在則直接返回,若是不存在則到內存中取出並保存在高速緩存中。如今多核處理器越基本已經成爲標配,這時每一個處理器都有本身的緩存,這就涉及到了緩存一致性的問題,CPU有不一樣強弱的一致性模型,最強的一致性安全性最高,也符合咱們的順序思考的模式,可是在性能上由於須要不一樣CPU之間的協調通訊就會有不少開銷。併發
典型的CPU緩存結構示意圖以下intellij-idea
CPU的指令週期一般爲取指令、解析指令讀取數據、執行指令、數據寫回寄存器或內存。串行執行指令時其中的讀取存儲數據部分佔用時間較長,因此CPU廣泛採起指令流水線的方式同時執行多個指令, 提升總體吞吐率,就像工廠流水線同樣。
讀取數據和寫回數據到內存相比執行指令的速度不在一個數量級上,因此CPU使用寄存器、高速緩存做爲緩存和緩衝,在從內存中讀取數據時,會讀取一個緩存行(cache line)的數據(相似磁盤讀取讀取一個block)。數據寫回的模塊在舊數據沒有在緩存中的狀況下會將存儲請求放入一個store buffer中繼續執行指令週期的下一個階段,若是存在於緩存中則會更新緩存,緩存中的數據會根據必定策略flush到內存。
public class MemoryModel { private int count; private boolean stop; public void initCountAndStop() { count = 1; stop = false; } public void doLoop() { while(!stop) { count++; } } public void printResult() { System.out.println(count); System.out.println(stop); }}
上面這段代碼執行時咱們可能認爲count = 1
會在stop = false
前執行完成,這在上面的CPU執行圖中顯示的理想狀態下是正確的,可是要考慮上寄存器、緩存緩衝的時候就不正確了, 例如stop自己在緩存中可是count不在,則可能stop更新後再count的write buffer寫回以前刷新到了內存。
另外CPU、編譯器(對於Java通常指JIT)均可能會修改指令執行順序,例如上述代碼中count = 1和stop = false二者並無依賴關係,因此CPU、編譯器都有可能修改這二者的順序,而在單線程執行的程序看來結果是同樣的,這也是CPU、編譯器要保證的as-if-serial(無論如何修改執行順序,單線程的執行結果不變)。因爲很大部分程序執行都是單線程的,因此這樣的優化是能夠接受而且帶來了較大的性能提高。可是在多線程的狀況下,若是沒有進行必要的同步操做則可能會出現使人意想不到的結果。例如在線程T1執行完initCountAndStop方法後,線程T2執行printResult,獲得的多是0, false, 多是1, false, 也多是0, true。若是線程T1先執行doLoop(),線程T2一秒後執行initCountAndStop, 則T1可能會跳出循環、也可能因爲編譯器的優化永遠沒法看到stop的修改。
因爲上述這些多線程狀況下的各類問題,多線程中的程序順序已經不是底層機制中的執行順序和結果,編程語言須要給開發者一種保證,這個保證簡單來講就是一個線程的修改什麼時候對其餘線程可見,所以Java語言提出了JavaMemoryModel即Java內存模型,對於Java語言、JVM、編譯器等實現者須要按照這個模型的約定來進行實現。Java提供了Volatile、synchronized、final等機制來幫助開發者保證多線程程序在全部處理器平臺上的正確性。
在JDK1.5以前,Java的內存模型有着嚴重的問題,例如在舊的內存模型中,一個線程可能在構造器執行完成後看到一個final字段的默認值、volatile字段的寫入可能會和非volatile字段的讀寫重排序。
因此在JDK1.5中,經過JSR133提出了新的內存模型,修復以前出現的問題。
其中普通讀指getfield, getstatic, 非volatile數組的arrayload, 普通寫指putfield, putstatic, 非volatile數組的arraystore。
volatile讀寫分別是volatile字段的getfield, getstatic和putfield, putstatic。
monitorenter是進入同步塊或同步方法,monitorexist指退出同步塊或同步方法。
上述表格中的No指前後兩個操做不容許重排序,如(普通寫, volatile寫)指非volatile字段的寫入不能和以後任意的volatile字段的寫入重排序。當沒有No時,說明重排序是容許的,可是JVM須要保證最小安全性-讀取的值要麼是默認值,要麼是其餘線程寫入的(64位的double和long讀寫操做是個特例,當沒有volatile修飾時,並不能保證讀寫是原子的,底層可能將其拆分爲兩個單獨的操做)。
final字段有兩個額外的特殊規則
1.final字段的寫入(在構造器中進行)以及final字段對象自己的引用的寫入都不能和後續的(構造器外的)持有該final字段的對象的寫入重排序。例如, 下面的語句是不能重排序的
x = sharedRef; ...; i = x.finalField
2.final字段的第一次加載不能和持有這個final字段的對象的寫入重排序,例以下面的語句是不容許重排序的
x = sharedRef; ...; i = x.finalField
處理器都支持必定的內存屏障(memory barrier)或柵欄(fence)來控制重排序和數據在不一樣的處理器間的可見性。例如,CPU將數據寫回時,會將store請求放入write buffer中等待flush到內存,能夠經過插入barrier的方式防止這個store請求與其餘的請求重排序、保證數據的可見性。能夠用一個生活中的例子類比屏障,例如坐地鐵的斜坡式電梯時,你們按順序進入電梯,可是會有一些人從左側繞過去,這樣出電梯時順序就不相同了,若是有一我的攜帶了一個大的行李堵住了(屏障),則後面的人就不能繞過去了:)。另外這裏的barrier和GC中用到的write barrier是不一樣的概念。
內存屏障的分類
幾乎全部的處理器都支持必定粗粒度的barrier指令,一般叫作Fence(柵欄、圍牆),可以保證在fence以前發起的load和store指令都能嚴格的和fence以後的load和store保持有序。一般按照用途會分爲下面四種barrier
LoadLoad Barriers
Load1; LoadLoad; Load2;
保證Load1的數據在Load2及以後的load前加載
StoreStore Barriers
Store1; StoreStore; Store2
保證Store1的數據先於Store2及以後的數據 在其餘處理器可見
LoadStore Barriers
Load1; LoadStore; Store2
保證Load1的數據的加載在Store2和以後的數據flush前
StoreLoad Barriers
Store1; StoreLoad; Load2
保證Store1的數據在其餘處理器前可見(如flush到內存)先於Load2和以後的load的數據的加載。StoreLoad Barrier可以防止load讀取到舊數據而不是最近其餘處理器寫入的數據。
幾乎近代的全部的多處理器都須要StoreLoad,StoreLoad的開銷一般是最大的,而且StoreLoad具備其餘三種屏障的效果,因此StoreLoad能夠當作一個通用的(可是更高開銷的)屏障。
因此,利用上述的內存屏障,能夠實現上面表格中的重排序規則
爲了支持final字段的規則,須要對final的寫入增長barrier
x.finalField = v; StoreStore; sharedRef = x;
插入內存屏障
基於上面的規則,能夠在volatile字段、synchronized關鍵字的處理上增長屏障來知足內存模型的規則
volatile store前插入StoreStore屏障
全部final字段寫入後但在構造器返回前插入StoreStore
volatile store後插入StoreLoad屏障
在volatile load後插入LoadLoad和LoadStore屏障
monitor enter和volatile load規則一致,monitor exit 和volatile store規則一致。
前面提到的各類內存屏障對應開發者來講仍是比較複雜底層,所以JMM又可使用一系列HappenBefore的偏序關係的規則方式來講明,要想保證執行操做B的線程看到操做A的結果(不管A和B是否在同一個線程中執行), 那麼在A和B之間必需要知足HappenBefore關係,不然JVM能夠對它們任意重排序。
HappendBefore規則包括
程序順序規則: 若是程序中操做A在操做B以前,那麼同一個線程中操做A將在操做B以前進行
監視器鎖規則: 在監視器鎖上的鎖操做必須在同一個監視器鎖上的加鎖操做以前執行
volatile變量規則: volatile變量的寫入操做必須在該變量的讀操做以前執行
線程啓動規則: 在線程上對Thread.start的調用必須在該線程中執行任何操做以前執行
線程結束規則: 線程中的任何操做都必須在其餘線程檢測到該線程已經結束以前執行
中斷規則: 當一個線程在另外一個線程上調用interrupt時,必須在被中斷線程檢測到interrupt以前執行
傳遞性: 若是操做A在操做B以前執行,而且操做B在操做C以前執行,那麼操做A在操做C以前執行。
其中顯示鎖與監視器鎖有相同的內存語義,原子變量與volatile有相同的內存語義。鎖的獲取和釋放、volatile變量的讀取和寫入操做知足全序關係,因此可使用volatile的寫入在後續的volatile的讀取以前進行。
能夠利用上述HappenBefore的多個規則進行組合。
例如線程A進入監視器鎖後,在釋放監視器鎖以前的操做根據程序順序規則HappenBefore於監視器釋放操做,而監視器釋放操做HappenBefore於後續的線程B的對相同監視器鎖的獲取操做,獲取操做HappenBefore與線程B中的操做。
近期熱文推薦:
1.Java 15 正式發佈, 14 個新特性,刷新你的認知!!
2.終於靠開源項目弄到 IntelliJ IDEA 激活碼了,真香!
3.我用 Java 8 寫了一段邏輯,同事直呼看不懂,你試試看。。
以爲不錯,別忘了隨手點贊+轉發哦!