【並發編程】- 記憶體模型(針對JSR-133記憶體模型)篇
- 2021 年 1 月 6 日
- 筆記
並發編程模型
-
1.兩個關鍵問題
-
1)執行緒之間如何通訊
-
共享記憶體
程之間共享程式的公共狀態,通過寫-讀記憶體中的公共狀態進行隱式通訊
-
消息傳遞
程之間沒有公共狀態,執行緒之間必須通過發送消息來顯式進行通訊
-
2)執行緒之間如何同步
-
執行緒之間沒有公共狀態,執行緒之間必須通過發送消息來顯式進行通訊
總結:Java的並發採用的是共享記憶體模型,Java執行緒之間的通訊總是隱式進行,整個通訊過程對程式設計師完全透明。
-
2.抽象結構
-
1)本地記憶體
- 每個執行緒都有一個私有的本地記憶體(LocalMemory),本地記憶體中存儲了該執行緒以讀/寫共享變數的副本。本地記憶體是JMM的一個抽象概念,並不真實存在。它涵蓋了快取、寫緩衝區、暫存器以及其他的硬體和編譯器優化
-
2)主記憶體
- 執行緒之間的共享變數存儲在主記憶體
附圖:
註:所有實例域、靜態域和數組元素都存儲在堆記憶體中,堆記憶體在執行緒之間共享(本章用「共享變數」這個術語代指實例域,靜態域和數組元素)。局部變數(Local Variables),方法定義參數(Java語法規範稱之為Formal Method Parameters)和異常處理器參數(Exception HandlerParameters)不會在執行緒之間共享,它們不會有記憶體可⻅性問題,也不受記憶體模型的影響。
-
3.重排序
- 定義:
重排序是指編譯器和處理器為了優化程式性能而對指令序列進行重新排序的一種手段。
-
1) 3種類型
- 編譯器優化的重排序(處理器重排序)
- 編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序
- 指令級並行的重排序(處理器重排序)
現代處理器採用了指令級並行技術(Instruction-Level Parallelism,ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序
記憶體系統的重排序
由於處理器使用快取和讀/寫緩衝區,這使得載入和存儲操作看上去可能是在亂序執行
-
2)數據依賴性
- 說明:如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據依賴性。
-
存在三種類型(只要重排兩個操作執行順序,結果便會被改變)
- 寫後讀:寫一個變數之後,再讀這個變數
- 寫後寫:寫一個變數之後,在寫這個變數
- 讀後寫:讀一個變數之後,再寫這個變數
-
3)as-if-serial語義
- 說明:不管怎麼重排序(編譯器和處理器為了提高並行度),(單執行緒)程式的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。
- 順序規則:A happens-before B,B happens-before C,happens-before C
那麼實際執行是B可以排在A前
,JMM允許這種排序。
- 順序規則:A happens-before B,B happens-before C,happens-before C
-
4)對多執行緒的影響
- 對於存證控制依賴的操作重排序,可能會改變程式的執行結果。
-
5)DCL問題(double check lock)
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
}
- 那麼問題就出現在執行緒執行到第4行,程式碼讀取到instance不為null時,instance引用的對象有可能還
沒有完成初始化。- __根源:
memory = allocate(); //1:分配對象的記憶體空間
ctorInstance(memory); //2:初始化對象
instance = memory; //3:設置instance指向剛分配的記憶體地址
- JMM允許上述命令的執行順序調整為
memory = allocate(); //1:分配對象的記憶體空間
instance = memory; //3:設置instance指向剛分配的記憶體地址
//注意,此時對象還沒有被初始化!
ctorInstance(memory); //2:初始化對象
- 問題:為什麼要調整這個順序呢?
- 原因:這個重排序在沒有改變單執行緒程式執行結果的前提下,可以提高程式的執行性能。
-
解決方案
- 不允許2和3重排序([
volatile
]); - 允許2和3重排序,但不允許其他執行緒「看到」這個重排序。
- 不允許2和3重排序([
-
第一種基於
volatile
方案
public class SafeDoubleCheckedLocking {
private volatile static Instance instance;
public static Instance getInstance() {
if (instance == null) {
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null)
instance = new Instance(); // instance為volatile,現在沒問題了
}
}
return instance;
}
}
- 第二種基於類初始化方案
public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance = new Instance();
}
public static Instance getInstance() {
return InstanceHolder.instance; // 這裡將導致InstanceHolder類被初始化,存在初始化鎖,拿不到的執行緒會一直等待
}
}
-
4.happens-before
happens-before是JMM最核心的概念
1) 關係的定義
- 如果⼀個操作happens-before另⼀個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。[
JMM對程式設計師的承諾
] - 兩個操作之間存在happens-before關係,並不意味著Java平台的具體實現必須要按照happens-before關係指定的順序來執行。如果重排序之後的執行結果,與按happens-before關係來執行的結果一致,那麼這種重排序並不非法(也就是說,JMM允許這種重排序)[
JMM對編譯器和處理器重排序的約束原則
]
2) 規則
- 程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中的任意後續操作。
- 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
- volatile變數規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
- 傳遞性:A happens-before B,且B happens-before C,那麼A happens-before C。
- start()規則:如果執行緒A執行操作ThreadB.start()(啟動執行緒B),那麼A執行緒的ThreadB.start()操作happens-before於執行緒B中的任意操作。
- join()規則:如果執行緒A執行操作ThreadB.join()並成功返回,那麼執行緒B中的任意操作happens-before於執行緒A從ThreadB.join()操作成功返回。
注意:兩個操作之間具有happens-before關係,並不意味著前
一個操作必須要在後一個操作之前執行!happens-before僅僅要求前一個操
作(執行的結果)對後一個操作可見,且前一個操作按順序排在第一個操
作之前(the first is visible to and ordered before the second)。
- appens-before與JMM的關係
話外語:
- as-if-serial語義保證單執行緒內程式的執行結果不被改變,happens-before關係保證正確同步的多執行緒程式的執行結果不被改變
- as-if-serial語義和happens-before這麼做的目的,都是為了在不改變程式執行結果的前提下,儘可能地提高程式執行的並行度