成神之路 第002期 JVM-Java內存模型
- 並發編程模型的分類
- 線程通信機制
- 共享內存(Java采用)
- 通過主內存和線程公共內存之間的信息同步來實現隱式通信
線程之間共享程序的公共狀態,線程之間通過寫-讀內存中的公共狀態來隱式進行通信。
- 通過主內存和線程公共內存之間的信息同步來實現隱式通信
- 消息機制
- 線程之間的通信必須通過明確的發送消息來顯式進行通信
- 共享內存(Java采用)
- 同步
- 程序用於控制不同線程之間操作發生相對順序的機制
- 在共享內存並發模型中,同步是顯式進行的。必須顯式的指定某個方法或代碼塊需要在先出現之間互斥執行
- 在消息傳遞的並發模型中同步是隱式進行的。因為消息的發送必須要在消息的接受之前
- 線程通信機制
- Java內存模型抽象
- 概念
- JMM(java memory model)定義了線程和主內存之間的抽象關系:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩沖區,寄存器以及其他的硬件和編譯器優化。
- 抽象模型圖
- 實例(線程A與線程B通信)
- 步驟一,線程A把本地內存A中更新過的共享變量刷新到主內存中去
- 步驟二,線程B到主內存中去讀取線程A之前已更新過的共享變量
- 數據競爭
- 在一個線程中寫一個變量
- 在另一個線程讀同一個變量
- 而且寫和讀沒有通過同步來排序
- JMM對正確同步的多線程程序的內存一致性做了如下保證
如果程序是正確同步的,程序的執行將具有順序一致性(sequentially consistent)。
即程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。這里的同步是指廣義上的同步,
包括對常用同步原語(lock,volatile和final)的正確使用。
- 順序一致性內存模型
- 一個線程中的所有操作必須按照程序的順序來執行。
- (不管程序是否同步)所有線程都只能看到一個單一的操作執行順序。在順序一致性內存模型中,每個操作都必須原子執行且立刻對所有線程可見。
- 示例圖
當多個線程並發執行時,圖中的開關裝置能把所有線程的所有內存讀/寫操作串行化。
- 順序一致性內存模型和JMM區別
- 順序一致性模型保證單線程內的操作會按程序的順序執行,而JMM不保證單線程內的操作會按程序的順序執行
- 順序一致性模型保證所有線程只能看到一致的操作執行順序,而JMM不保證所有線程能看到一致的操作執行順序
- JMM不保證對64位的long型和double型變量的讀/寫操作具有原子性,而順序一致性模型保證對所有的內存讀/寫操作都具有原子性
- 概念
- 重排序
- 重排序分類
- 編譯器優化的重排序
- 編譯器在不改變的語義的前提下,對執行語句的順序做出調整
- 指令級並行的重排序
- 現代處理器采用了指令級並行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。若數據不存在依賴性,處理器可以改變語句對應的機器指令的執行順序
- 內存系統的重排序
- 指令屏障
- 為了保證內存可見性,java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序
- 編譯器優化的重排序
- 數據依賴性
- 如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據依賴性。
- 上面三種情況,只要重排序兩個操作的執行順序,程序的執行結果將會被改變。
- 數據依賴性只是針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器和不同線程之間不做考慮。
- 如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據依賴性。
- as-if-serial語義
- 不管怎么重排序(編譯器和處理器為了提高並行度),(單線程)程序的執行結果不能被改變。
- 所有的重排序都不會有數據依賴的操作做重排序,因為這樣會改變最終的執行結果。
- 程序順序規則
- 在不改變程序執行結果的前提下,盡可能的開發並行度。
- 重排序對多線程的影響
- 當代碼中存在控制依賴性時,會影響指令序列執行的並行度。為此,編譯器和處理器會采用猜測(Speculation)執行來克服控制相關性對並行度的影響。
- 在單線程程序中,對存在控制依賴的操作重排序,不會改變執行結果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因);但在多線程程序中,對存在控制依賴的操作重排序,可能會改變程序的執行結果。
- 重排序分類
- volatile
- 自身特性
- 可見性:對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。
- 原子性:對任意單個volatile變量的讀/寫具有原子性,但類似於volatile++這種復合操作不具有原子性。
- 內存語義
- 當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存。
- 當讀一個volatile變量時,JMM會把該線程對應的本地內存置為無效。線程接下來將從主內存中讀取共享變量。
- 內存語義總結
- 線程A寫一個volatile變量,實質上是線程A向接下來將要讀這個volatile變量的某個線程發出了(其對共享變量所在修改的)消息。
- 線程B讀一個volatile變量,實質上是線程B接收了之前某個線程發出的(在寫這個volatile變量之前對共享變量所做修改的)消息。
- 線程A寫一個volatile變量,隨后線程B讀這個volatile變量,這個過程實質上是線程A通過主內存向線程B發送消息。
- 內存語義實現
- 當第二個操作是volatile寫時,不管第一個操作是什么,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后。
- 當第一個操作是volatile讀時,不管第二個操作是什么,都不能重排序。這個規則確保volatile讀之后的操作不會被編譯器重排序到volatile讀之前。
- 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。
- 基於保守策略的JMM內存屏障插入策略
- 在每個volatile寫操作的前面插入一個StoreStore屏障。
- 在每個volatile寫操作的后面插入一個StoreLoad屏障。
- 在每個volatile讀操作的后面插入一個LoadLoad屏障。
- 在每個volatile讀操作的后面插入一個LoadStore屏障。
-
- 自身特性
- 鎖
- 釋放和獲取的內存語義
- 當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。
- 當線程獲取鎖時,JMM會把該線程對應的本地內存置為無效。從而使得被監視器保護的臨界區代碼必須要從主內存中去讀取共享變量。
- 對比鎖釋放-獲取的內存語義與volatile寫-讀的內存語義
- 鎖釋放與volatile寫有相同的內存語義;鎖獲取與volatile讀有相同的內存語義。
- 總結
- 線程A釋放一個鎖,實質上是線程A向接下來將要獲取這個鎖的某個線程發出了(線程A對共享變量所做修改的)消息。
- 程B獲取一個鎖,實質上是線程B接收了之前某個線程發出的(在釋放這個鎖之前對共享變量所做修改的)消息。
- 線程A釋放鎖,隨后線程B獲取這個鎖,這個過程實質上是線程A通過主內存向線程B發送消息。
- 鎖釋放-獲取的內存語義的實現方式
- 利用volatile變量的寫-讀所具有的內存語義。
- 利用CAS(java的compareAndSet()方法)所附帶的volatile讀和volatile寫的內存語義。
CAS如何同時具有volatile讀和volatile寫的內存語義
編譯器不會對volatile讀與volatile讀后面的任意內存操作重排序
編譯器不會對volatile寫與volatile寫前面的任意內存操作重排序
- concurrent包的實現
- java的CAS同時具有 volatile 讀和volatile寫的內存語義,因此Java線程之間的通信現在有四種方式:
- A線程寫volatile變量,隨后B線程讀這個volatile變量。
- A線程寫volatile變量,隨后B線程用CAS更新這個volatile變量。
- A線程用CAS更新一個volatile變量,隨后B線程用CAS更新這個volatile變量。
- A線程用CAS更新一個volatile變量,隨后B線程讀這個volatile變量。
- java的CAS同時具有 volatile 讀和volatile寫的內存語義,因此Java線程之間的通信現在有四種方式:
- 釋放和獲取的內存語義
- final
- 遵守兩個重排序規則:
- 在構造函數內對一個final域的寫入,與隨后把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。
- 初次讀一個包含final域的對象的引用,與隨后初次讀這個final域,這兩個操作之間不能重排序。
- 寫final域的重排序規則:
- JMM禁止編譯器把final域的寫重排序到構造函數之外。
- 編譯器會在final域的寫之后,構造函數return之前,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構造函數之外。
- 讀final域的重排序規則:
- 在一個線程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操作(注意,這個規則僅僅針對處理器)。編譯器會在讀final域操作的前面插入一個LoadLoad屏障。
- final域是引用類型
- 示例代碼
public class FinalReferenceExample {
final int[] intArray; //final是引用類型
static FinalReferenceExample obj;
public FinalReferenceExample () { //構造函數
intArray = new int[1]; //1
intArray[0] = 1; //2
}
public static void writerOne () { //寫線程A執行
obj = new FinalReferenceExample (); //3
}
public static void writerTwo () { //寫線程B執行
obj.intArray[0] = 2; //4
}
public static void reader () { //讀線程C執行
if (obj != null) { //5
int temp1 = obj.intArray[0]; //6
}
}
} - 實例圖片
- 上圖,1是對final域的寫入,2是對這個final域引用的對象的成員域的寫入,3是把被構造的對象的引用賦值給某個引用變量。這里除了前面提到的1不能和3重排序外,2和3也不能重排序。
- JMM可以確保讀線程C至少能看到寫線程A在構造函數中對final引用對象的成員域的寫入。即C至少能看到數組下標0的值為1。而寫線程B對數組元素的寫入,讀線程C可能看的到,也可能看不到。JMM不保證線程B的寫入對讀線程C可見,因為寫線程B和讀線程C之間存在數據競爭,此時的執行結果不可預知。
- 示例代碼
- 遵守兩個重排序規則: