JMM 全稱,Java Memory Model. 這個內存模型與Stack,heap GC分代的內存模型,不是一回事,二者是經過不通的維度,將硬件訪問抽象出來的一層抽象的邏輯模型,JVM屏蔽了硬件的直接操做。 GC分代的內存模型更加貼近與垃圾回收和內存分配使用的理解,而JMM模型更加貼近,多線程和內存之間的通信數組
Java內存模型規定了全部的變量都存儲在主內存(Main Memory)中。每條線程還有本身的工做內存,線程的工做內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主內存來完成。安全
須要注意的是, 放在住內存的變量包括,實例字段、靜態字段和構成數組對象的元素,但不包括局部變量與方法參數,由於後者是線程獨有的。多線程
線程間若是要完成變成的同步和共享,必須經歷下面2個步驟。併發
1. 線程A必需要把線程A的工做內存更新過的變量刷新到主內存去。app
2. 線程B到主內存中去讀取線程A更新過的共享變量工具
這些通信操做是被JMM屏蔽的,要保證變量的線程安全共享 須要使用Java的同步塊(synchonrized),或者其餘併發工具。這裏強調的是安全共享,在不加同步塊,和併發工具的狀況下,變量也是能夠被共享的,只是不能保證讀都最新數據,就是常說的 髒讀,錯讀等優化
根據工做內存與住內存的交互協議,一個 變量從主內存拷貝到工做內存,與工做內存同步到主內存,會通過一下八個原子操做。this
lock | 主存 | 做用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態。 |
unlock | 主存 | 做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。 |
read | 主存 | 做用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用。 |
load | 工做內存 | 做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中 |
use | 工做內存 | 做用於工做內存的變量,它把工做內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個須要使用到變量的值的字節碼指令時將會執行這個操做 |
assign | 工做內存 | 做用於工做內存的變量,它把一個從執行引擎接收到的值賦給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。 |
store | 工做內存 | 做用於工做內存的變量,它把工做內存中一個變量的值傳送到主內存中,以便隨後的write操做使用。 |
write | 主內存 | 做用於主內存的變量,它把store操做從工做內存中獲得的變量的值放入主內存的變量中。 |
JMM規定若是要把一個變量從主內存複製到工做內存,那就要順序地執行read和load操做,若是要把變量從工做內存同步回主內存,就要順序地執行store和write操做。spa
注意,Java內存模型只要求上述兩個操做必須按順序執行,而沒有保證是連續執行。也就是說,read與load之間、store與write之間是可插入其餘指令的,如對主內存中的變量a、b進行訪問時,一種可能出現順序是read a、read b、load b、load a。 線程
除了以上的順序約束之外,還規定了其餘的約束:
a. 不容許read和load、store和write操做之一單獨出現,即不容許一個變量從主內存讀取了但工做內存不接受,或者從工做內存發起回寫了但主內存不接受的狀況出現。
b. 不容許一個線程丟棄它的最近的assign操做,即變量在工做內存中改變了以後必須把該變化同步回主內存。
c. 不容許一個線程無緣由地(沒有發生過任何assign操做)把數據從線程的工做內存同步回主內存中。
d. 一個新的變量只能在主內存中「誕生」,不容許在工做內存中直接使用一個未被初始化(load或assign)的變量,換句話說,就是對一個變量實施use、store操做以前,必須先執行過了assign和load操做。
e. 一個變量在同一個時刻只容許一條線程對其進行lock操做,但lock操做能夠被同一條線程重複執行屢次,屢次執行lock後,只有執行相同次數的unlock操做,變量纔會被解鎖。
f. 若是對一個變量執行lock操做,那將會清空工做內存中此變量的值,在執行引擎使用這個變量前,須要從新執行load或assign操做初始化變量的值。
g. 若是一個變量事先沒有被lock操做鎖定,那就不容許對它執行unlock操做,也不容許去unlock一個被其餘線程鎖定住的變量。 對一個變量執行unlock操做以前,必須先把此變量同步回主內存中(執行store、write操做)。
關鍵字volatile能夠說是Java虛擬機提供的最輕量級的同步機制. 當一個變量定義爲volatile以後,它將具有兩種特性,第一是保證此變量對全部線程的可見性. 第二個語義是禁止指令重排序優化.
這裏的「可見性」是指當一條線程修改了這個變量的值,新值對於其餘線程來講是能夠當即得知的。而普通變量不能作到這一點,普通變量的值在線程間傳遞均須要經過主內存來完成,例如,線程A修改一個普通變量的值。須要注意的是,volatile只是保證了可見性,很容易誤解爲volatile變量在各個線程中是一致的,因此基於volatile變量的運算在併發下是安全的。可是Java裏面的運算並不是原子操做,各個工做區的volaitile的變量可能存在不一致的狀況,致使volatile變量的運算在併發下同樣是不安全的。
Java內存模型是圍繞着在併發過程當中如何處理原子性、可見性和有序性這3個特徵來創建的
原子性(Atomicity):由Java內存模型來直接保證的原子性變量操做包括read、load、assign、use、store和write,咱們大體能夠認爲基本數據類型的訪問讀寫是具有原子性的(例外就是long和double的非原子性協定,讀者只要知道這件事情就能夠了,無須太過在乎這些幾乎不會發生的例外狀況)。 若是應用場景須要一個更大範圍的原子性保證(常常會遇到),Java內存模型還提供了lock和unlock操做來知足這種需求,儘管虛擬機未把lock和unlock操做直接開放給用戶使用,可是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱式地使用這兩個操做,這兩個字節碼指令反映到Java代碼中就是同步塊——synchronized關鍵字,所以在synchronized塊之間的操做也具有原子性。
可見性(Visibility):可見性是指當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。Java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存做爲傳遞媒介的方式來實現可見性的,不管是普通變量仍是volatile變量都是如此,普通變量與volatile變量的區別是,volatile的特殊規則保證了新值能當即同步到主內存,以及每次使用前當即從主內存刷新。所以,能夠說volatile保證了多線程操做時變量的可見性,而普通變量則不能保證這一點。
除了volatile以外,Java還有兩個關鍵字能實現可見性,即synchronized和final。同步塊的可見性是由「對一個變量執行unlock操做以前,必須先把此變量同步回主內存中(執行store、write操做)」這條規則得到的,而final關鍵字的可見性是指:被final修飾的字段在構造器中一旦初始化完成,而且構造器沒有把「this」的引用傳遞出去(this引用逃逸是一件很危險的事情,其餘線程有可能經過這個引用訪問到「初始化了一半」的對象),那在其餘線程中就能看見final字段的值。
有序性(Ordering):Java內存模型的有序性在前面講解volatile時也詳細地討論過了,Java程序中自然的有序性能夠總結爲一句話:若是在本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另外一個線程,全部的操做都是無序的。前半句是指「線程內表現爲串行的語義」(Within-Thread As-If-Serial Semantics),後半句是指「指令重排序」現象和「工做內存與主內存同步延遲」現象。 Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操做的有序性,volatile關鍵字自己就包含了禁止指令重排序的語義,而synchronized則是由「一個變量在同一個時刻只容許一條線程對其進行lock操做」這條規則得到的,這條規則決定了持有同一個鎖的兩個同步塊只能串行地進入。
若是Java內存模型中全部的有序性都僅僅靠volatile和synchronized來完成,那麼有一些操做將會變得很煩瑣,可是咱們在編寫Java併發代碼的時候並無感受到這一點,這是由於Java語言中有一個「先行發生」(happens-before)的原則。這個原則很是重要,它是判斷數據是否存在競爭、線程是否安全的主要依據,依靠這個原則,咱們能夠經過幾條規則一攬子地解決併發環境下兩個操做之間是否可能存在衝突的全部問題。