Java記憶體模型之簡要知識與規範梳理

  • 2020 年 3 月 10 日
  • 筆記

1. JMM簡要知識

語義規範

  • Java程式語言的語義允許編譯器和微處理器執行優化,從而與不正確同步的程式碼進行交互來完成工作.
  • 執行緒內語義是單執行緒程式的語義,它允許根據執行緒內讀操作看到的值來完全預測執行緒的行為;由於單執行緒內的實現是在其上下文執行的, 可以通過評估執行緒的實現從而確定執行中的執行緒的操作是否合法.
  • 程式遵循執行緒內語義: 在執行緒隔離狀態下,每個執行緒的操作必須由該執行緒的語義控制,但是每個讀操作看到的值由記憶體模型決定.

JMM規範

  • 從數據存儲上,對於共享數據的讀寫操作,JMM會通過執行緒工作記憶體以及JVM的堆記憶體來對數據進行讀寫操作(類比互聯網業務,工作記憶體類比為redis等快取,堆記憶體類比為數據儲存載體,如資料庫)
  • 從程式碼優化上,JMM為了提升性能,會針對程式順序程式碼進行重排序甚至刪除不必要的同步程式碼

JMM概要

  • 給定程式以及一個檢測程式是否合法的執行跟蹤,JMM工作原理是檢查執行跟蹤中的每個讀,並根據某些規則檢查讀觀察到的寫是否有效
  • 主要保證執行的每個結果與記憶體模型預期值一致,那麼它可以不關心實現者是如何實現程式的行為
  • 記憶體模型決定在程式的每個點可以讀取哪些值。在隔離狀態下,每個執行緒的操作必須由該執行緒的語義控制,但是每個讀操作看到的值由記憶體模型決定
  • 每次在執行緒內對變數進行寫行為產生一個執行緒間的動作時,它必須匹配程式順序中緊隨其後的讀行為的執行緒間動作,其中對於執行緒的讀操作行為獲取的值是由於JMM決定的值
2. JMM與順序一致性模型

程式順序與順序一致性

  • 程式順序
    • 可描述為執行緒間所有動作是根據執行緒內語義執行操作順序的一個集合
    • 簡言之,就是在執行緒內的操作所見即所得,即程式程式碼順序
  • 順序一致性記憶體模型
    • 一個執行緒所有操作都必須按照程式的順序來執行
    • 不論執行緒是否同步,所有執行緒都只能看到一個單一的操作執行順序,並且每個操作都必須是原子性操作並立即對其他所有執行緒可見
  • 順序一致性問題
    • 如果記憶體模型使用一致性模型,則將會導致編譯器和處理器的優化策略變得不合法

JMM在順序一致性方面的努力

  • 源程式碼
// shared.java  int pwrite = 0;  int cwrite = 0;  // producer.java  int pread = 0;  int r1 = 0;  run(){      r1 = 20;				// --- 1  	pread = cwrite;			// --- 2  	pwrite = 10;			// --- 3  }  // consumer.java  int cread = 0;  int r2 = 0;  run(){  	cread = pwrite;			// --- 4  	r2 = 21;				// --- 5  	cwrite = 20;			// --- 6  }
  • 程式碼分析
    • 基於JMM模型: 由於存在數據競爭,上面的程式碼執行順序會在編譯器階段,JMM允許對程式程式碼進行重排序,輸出結果會出現pread = cwrite = 20 與 cread = pwrite = 10的情況
    • 基於一致性記憶體模型: 將會正常輸出,不會出現pread = cwrite = 20 與 cread = pwrite = 10的情況,但是執行緒之間的順序會交替執行
  • 加鎖方案
    • 基於JMM模型: 保證輸出結果的正常,但是在上述執行緒內執行的順序會被重排序
    • 一致性記憶體模型: 不會打亂順序,仍然正常結果輸出
  • 小結
    • 在存在數據競爭的條件下,JMM無法保證執行緒之間的執行順序,而順序一致性保證與程式碼執行的順序相同,即使執行緒的執行順序存在交替執行也不影響單個執行緒內的執行順序
    • 單個執行緒中,JMM仍然會對臨界區的執行動作進行重排序,而順序一致性並沒有進行重排,仍然保持與程式程式碼相同的順序
3. JMM規範梳理

共享數據規則

  • 能夠被多個執行緒共享的記憶體區域稱為共享記憶體或是堆記憶體
  • 執行緒共享數據: 所有的對象實例欄位,static欄位,數組元素等
  • 執行緒封閉數據: 局部變數,方法參數,異常處理器以及ThreadLocal/ThreadLocalRandom等

執行緒操作規則

相當於執行緒行為可以被其他執行緒看到,也可以檢測到其他執行緒的行為動作,程式行為表現如下:

  • 可以執行正常的讀操作
  • 可以執行正常的寫操作
  • 對於同步程式碼塊
    • 可以執行volatile數據的讀取,說明其他執行緒的寫操作當前執行緒可以「看到」(寫操作在執行緒失效直接讀取主記憶體)
    • 可以執行volatile數據的寫,說明變數數據可以對其他執行緒「可見」
    • lock鎖定監視器
    • unlock解鎖監視器
  • 執行緒合併執行的第一個和最後一個動作(個人理解為等待多個執行緒執行子任務之後再一同執行程式後續程式碼的場景)
  • 啟動執行緒和終止執行緒

Synchronization原則(能夠被感知,可見行為的變化)

  • 監視器m的解鎖與監視器m的後續動作加鎖操作同步
  • 執行緒對volatile變數v進行寫操作,與任何執行緒對v的所有後續讀操作同步
  • 啟動執行緒的操作與執行緒執行的第一個動作的操作同步
  • 在執行緒中對每個屬性執行默認值的寫入操作與執行緒的第一個動作操作同步
  • 執行緒中的最終動作T1 與另一個執行緒T2中檢測到T1已終止的任何動作同步
  • 如果執行緒T1中斷thread T2,則該中斷執行緒T1 將與 任何其他執行緒(包括T2)確定T2已被中斷(通過InterruptedException引發或調用Thread.interrupted 或Thread.isInterrupted)的任何點同步

Happen-Before原則(規範)

  • 執行動作之間的happen-before關係 如果兩個動作x和y,我們定義hb(x,y)來描述x happen before y,滿足關係有以下情況:
    • 同一個執行緒中執行動作x和y,在程式順序上x優先於y,則hb(x,y)
    • 對象構造器程式碼塊的結束happen-before該對象finalizer的開始
    • 如果x動作與接下來的y動作同步,則hb(x,y)
    • 傳遞性,如果hb(x,y),且hb(y,z),則hb(x,z)
  • happen-before原則 主要作用於兩個的動作存在衝突的執行順序以及定義數據競爭發生的時機,具體VM實現,遵循以下原則:
    • 執行緒解鎖動作都happen-before該執行緒的後續動作鎖操作
    • 對volatile變數執行寫操作happen-before該變數讀操作的後續每個動作
    • 調用執行緒的start()方法happen-before於已開啟的執行緒內的任何一個動作
    • 執行緒所有動作happen-before於其他任意執行緒成功從該執行緒對象的join()方法返回
    • 程式任何對象的初始化happen-before於程式中任何其他的動作操作行為
  • 作用
    • 遵循上述的原則,意味著有些程式碼不能進行重排序,有些數據不能被快取(解決JMM可見性的規範)

感謝花時間閱讀,如果有用歡迎轉發或者點個好看,謝謝!!!