Java內存模型的基礎
本文是《java併發編程的藝術》一書的學習筆記
1.Java內存模型的抽象結構
1.Java線程之間的通訊由Java內存模型(JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。
2.線程之間的共享變量存儲在主內存中,每個線程都有一個私有的本地內存,本地內存存儲了該線程以讀/寫共享變量的副本。本地內存是一個抽象概念,並不真實存在。
3.Java內存模型的抽象示意圖如下
如果線程A與線程B之間要通訊的話,需要經歷下面兩個步驟。
- 1.線程A把本地內存A中更新的共享變量刷新到主內存中去。
- 2.線程B到主內存中去讀取線程A之前已更新過的共享變量。
2.happens-before簡介
1.從JDK5開始,Java使用新的JSR-133內存模型。JSR-133使用happens-before的概念來闡述操作之間的內存可見性。
2.在JMM中如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須存在happens-before關係。這裏的操作既可以是在一個線程內,也可以在不同線程之間。
3.與程序員密切相關的happens-before規則如下
- 程序順序規則:一個線程中的每個操作,happens-before於該線程中的任意後續操作。
- 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖加鎖。
- volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
- 傳遞性:如果 A happens-before B, 且 B happens-before C,那麼 A happens-before C.
注意
兩個操作之間具有happens-before關係,並不意味着前一個操作必須要在後一個操作之前執行! happens-before僅僅要求前一個操作(執行的結果)對後一個操作可見,且前一個操作按順序排在第二個操作之前
happens-before與JMM的關係如下:
Volatile內存語義
Volatile的特性
理解volatile特性的一個方法是把對volatile變量的單個讀/寫,看成是使用同一個鎖對這個讀/寫做了同步。下面通過具體示例說明:
假設有多個線程分別調用上面程序的3個方法,這個程序在語義上和下面的程序等價
如上面的程序所示,一個volatile變量的單個讀/寫操作,與一個普通變量的讀/寫操作是使用同一個鎖來同步,它們之間的執行效果相同。
volatile具有以下特性
- 可見性。 當線程A對volatile變量寫入時,該變量會刷新到主內存。然後當線程B讀取該volatile變量時,會在主內存中進行讀取,避免在自身的本地線程讀取。 對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入。
- 原子性。 對任意單個volatile變量的讀/寫具有原子性,但類似volatile++這種複合操作不具有原子性。
- 禁止編譯器處理器指令重排序。 嚴格限制編譯器處理器對volatile變量與普通變量的重排序,確保volatile的讀/寫和鎖的釋放/獲取具有相同的內存語義。
volatile內存語義的實現
爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
下面是基於保守策略的JMM內存屏障插入策略。
- 在每個volatile寫操作的前面插入一個StoreStore屏障
- 在每個volatile寫操作的後面插入一個StoreLoad屏障
- 在每個volatile讀操作的後面插入一個LoadLoad屏障
- 在每個volatile讀操作的後面插入一個LoadStore屏障
鎖的內存語義
1.當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中
2.當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效。從而使被監視器保護的臨界區代碼必須從主內存中讀取共享變量。
final域的內存語義
1.通過爲final域增加讀寫重排序規則,可以爲java程序員提供初始化安全保證:
只要對象是正確構造的(被構造對象的引用在構造函數中沒有“逸出”),那麼不需要使用同步(指lock和volatile的使用)就可以保證任意線程都能看到這個final域在構造函數中被初始化之後的值
2.爲什麼final引用不能從構造函數內"逸出"?
看下示例代碼:
假設線程A執行writer()方法,線程B執行reader()方法。這裏的操作2使得對象還未完成構造前就爲線程B可見。即使這裏的操作2是構造函數的最後一步,且在程序中操作2排在操作1後面,執行read()方法的線程仍然可能無法看到final域被初始化後的值,因爲這裏的操作1和操作2可能被重排序。
雙重檢查鎖定與延遲初始化
雙重檢查鎖定的由來
下面是示例代碼
上面的代碼是有問題的,問題的根源在第7行初始化的時候。
因爲初始化構造的時候會存在重排序
new DclSinglenton()初始化構造的時候可以分解如下三行僞代碼:
memory = allocate(); //1.分配對象的內存空間
ctorInstance(memory);//2.初始化對象
instance = memory;//3.設置instance指向剛分配的內存地址
多線程情況下2,3可能會重排序,如下:
memory = allocate(); //1.分配對象的內存空間
instance = memory;//3.設置instance指向剛分配的內存地址
//注意,此時對象還沒有被初始化
ctorInstance(memory);//2.初始化對象
第7行如果發生重排序,另一個併發執行的線程B就有可能在第4行判斷instance不爲null. 線程B接下來將訪問instance所引用的對象,但此時這個對象可能還沒有被A線程初始化!
解決方法:
1.不允許2和3重排序
2.允許2和3重排序,但不允許其他線程”看到“這個重排序。
基於volatile的解決方案
public class SafeDoubleCheckedLocking{
private volatile static Instance instance;//聲明volatile
public static Instance getInstance(){
if (instance == null){
synchronized (SafeDoubleCheckedLocking.class){
if (instance == null){
instance = new Instance(); //沒有問題
}
}
}
}
}
聲明對象的引用爲volatile後,禁止僞代碼中的2,3重排序
基於類初始化的解決方案
JVM在類的初始化階段(即在Class被加載後,且被線程使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化。
基於這個特性,可以實現另一種線程安全的延遲初始化方案
public class StaticInnerSingleton {
private StaticInnerSingleton() {
}
public static StaticInnerSingleton getInstance() {
return SingletonHolder.sInstance;
}
// 靜態內部類
private static class SingletonHolder {
private static final StaticInnerSingleton sInstance = new StaticInnerSingleton();
}
}
參考:
《java併發編程的藝術》