1. jmm屏蔽各類硬件和操做系統的內存訪問差別,以實現讓java程序在各類平臺下都能達到一致性的內存訪問效果。jmm解決的是一個線程修改一個變量,什麼時候對其餘線程可見的問題。涉及的關鍵字有volatile、final、鎖,經過這些能夠實現java的內存可見性。java
2. jmm定義的內存模型如圖:程序員
其實jvm並無本地內存、主內存的說法,只不過爲了讓人們更加理解jmm,屏蔽內部實現的複雜性而抽象出來的模型。安全
3. happens-beforeapp
在JMM中,若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必需要存在happens-before關係。兩個操做既能夠是一個線程中的,也能夠是兩個不一樣的線程中的。Happends-before的規則以下:jvm
A 程序順序規則:一個線程中的每一個操做,happens- before 於該線程中的任意後續操做。函數
B 監視器鎖規則:對一個監視器鎖的解鎖,happens- before 於隨後對這個監視器鎖的加鎖。學習
C volatile變量規則:對一個volatile域的寫,happens- before 於任意後續對這個volatile域的讀。spa
D 傳遞性:若是A happens- before B,且B happens- before C,那麼A happens- before C。操作系統
A能夠這麼理解,單線程中若是須要可見性的要求,即寫操做對以後讀操做的可見性,那必然出現了數據依賴關係,根據as-if-serial語義,不會出現重排序。線程
若是遵循這些規則,就能保證變量的可見性了。
對於java程序員來講,happens-before規則簡單易懂,它避免java程序員爲了理解JMM提供的內存可見性保證而去學習複製的重排序規則以及這些規則的具體實現。
重點注意:對兩個線程來講,爲了正確的設置happens-before關係,訪問相同的volatile變量是很重要的。如下的結論是不正確的:當線程A寫volatile字段f的時候,線程A可見的全部東西,在線程B讀取volatile的字段g以後,變得對線程B可見了。釋放操做和獲取操做必須匹配(也就是在同一個volatile字段上面完成)。
4. as-if-serial
無論怎麼重排序(編譯器和處理器爲了提升並行度),程序的執行結果不能被改變,編譯器,runtime和處理器都必須遵照as-if-serial語義。
爲了遵照as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操做作重排序,由於這種重排序會改變執行結果。
數據依賴性
若是兩個操做訪問同一個變量,且這兩個操做中有一個爲寫操做,此時這兩個操做之間就存在數據依賴性。數據依賴分下列三種類型:
名稱 |
代碼示例 |
說明 |
寫後讀 |
a = 1;b = a; |
寫一個變量以後,再讀這個位置。 |
寫後寫 |
a = 1;a = 2; |
寫一個變量以後,再寫這個變量。 |
讀後寫 |
a = b;b = 1; |
讀一個變量以後,再寫這個變量。 |
5. volatile的含義
volatile的特性
可見性:對一個volatile變量的讀,老是能看到(任意線程)對這個volatile變量的最後的寫入。
原子性:對任意單個volatile變量的讀/寫具備原子性,但相似於volatile++這個種符合操做不具備 原子性。
有序性:加入內存屏障,防止重排序。
volatile寫的內存語義:
當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存。
volatile讀的內存語義:
當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。
volatile的內存語義實現是經過內存屏障來實現的。
下面是JMM針對編譯器制定的volatile重排序規則表:
是否能重排序 |
第二個操做 |
||
第一個操做 |
普通讀/寫 |
volatile讀 |
volatile寫 |
普通讀/寫 |
|
|
NO |
volatile讀 |
NO |
NO |
NO |
volatile寫 |
|
NO |
NO |
從上表咱們能夠看出:
當第二個操做是volatile寫時,無論第一個操做是什麼,都不能重排序。這個規則確保volatile寫以前的操做不會被編譯器重排序到volatile寫以後。
當第一個操做是volatile讀時,無論第二個操做是什麼,都不能重排序。這個規則確保volatile讀以後的操做不會被編譯器重排序到volatile讀以前。
當第一個操做是volatile寫,第二個操做是volatile讀時,不能重排序。
下面是基於保守策略的JMM內存屏障插入策略:
在每一個volatile寫操做的前面插入一個StoreStore屏障
在每一個volatile寫操做的後面插入一個StoreLoad屏障
在每一個volatile讀操做的後面插入一個LoadLoad屏障
在每一個volatile讀操做的後面插入一個LoadStore屏障
上述內存屏障插入策略很是保守,但它能夠保證在任意處理器平臺,任意的程序中都能獲得正確的volatile內存語義。
爲了保證內存可見性,java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。JMM把內存屏障指令分爲下列四類:
屏障類型 |
指令示例 |
說明 |
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; Load2 |
確保Store1數據對其餘處理器變得可見(指刷新到內存),以前於Load2及全部後續裝載指令的裝載。StoreLoad Barriers會使該屏障以前的全部內存訪問指令(存儲和裝載指令)完成以後,才執行該屏障以後的內存訪問指令。 |
內存屏障有兩個做用:
1)確保一些特定操做執行的順序
2)影響一些數據的可見性
爲何doublecheck須要volitale
public class DoubleCheckedLocking { //1 private static Instance instance; //2 public static Instance getInstance() { //3 if (instance == null) { //4:第一次檢查 synchronized (DoubleCheckedLocking.class) { //5:加鎖 if (instance == null) //6:第二次檢查 instance = new Instance(); //7:問題的根源出在這裏 } //8 } //9 return instance; //10 } //11 }
前面的雙重檢查鎖定示例代碼的第7行(instance = new Singleton();)建立一個對象。這一行代碼能夠分解爲以下的三行僞代碼:
memory = allocate(); //1:分配對象的內存空間 ctorInstance(memory); //2:初始化對象 instance = memory; //3:設置instance指向剛分配的內存地址
因爲步驟2與步驟3沒有數據依賴關係 可能發生重排序,synchronized關鍵字並能保證內部不會發生重排序,因此另一個線程可能在第四步檢查不爲空,但實際對象尚未初始化成功,只是分配了內存空間,並install指向了內存空間的地址。
Instance設置成valotile後會禁止instance = new Singleton()內部的重排序。
我認爲instance不爲volatile的話,還會出現其餘的不穩定的因素。在第四步進行檢查的時候有可能不是最新的值,由於普通變量不能保證讀取到其餘線程最後一次寫入的值。
6. final
對於final域,編譯器和處理器要遵照兩個重排序規則:
1)在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。
2)初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操做之間不能重排序。
寫final域的重排序規則
寫final域的重排序規則禁止把final域的寫重排序到構造函數以外。這個規則的實現包含下面2個方面:
1)JMM禁止編譯器把final域的寫重排序到構造函數以外。
2)編譯器會在final域的寫以後,構造函數return以前,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構造函數以外。
讀final域的重排序規則:在一個線程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操做(注意,這個規則僅僅針對處理器)。編譯器會在讀final域操做的前面插入一個LoadLoad屏障。
對於final域只保證在構造函數中初始化的安全性,不保證後續對final引用的對象的修改的安全性。
注意:構造對象的引用不能提早在構造器中溢出,對其餘線程可見,由於final域可能尚未初始化
7. 鎖
鎖釋放和獲取的內存語義:
當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中
當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效。從而使得被監視器保護的臨界區代碼必需要從主內存中去讀取共享變量
AQS分公平鎖和非公平鎖。公平鎖是經過獲取鎖時讀取volatile變量,釋放鎖時寫入volatile變量實現可見性的;非公平鎖獲取鎖跟跟公平鎖不同,經過cas實現獲取鎖,cas具備volatile相同的語義,釋放鎖跟公平鎖同樣。
參考 http://www.infoq.com/cn/author/%E7%A8%8B%E6%99%93%E6%98%8E