java記憶體模型——重排序

leayun發表於2021-07-29

執行緒安全問題概括來說表現為三個方面:原子性,可見性和有序性。

在多核處理器的環境下:編譯器可能改變兩個操作的先後順序;處理器可能不是完全依照程式的目的碼所指定的順序執行命令;一個處理器執行的多個操作,在其他處理器的角度來看,其順序可能與目的碼所指定的順序不一致。這種現象就叫重排序。

在執行程式時,為了提高效能,編譯器和處理器常常會對指令做重排序。重排序分3種型別。

  1. 編譯器優化的重排序。編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。
  2. 指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 記憶體系統的重排序。由於處理器使用快取和讀/寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。

記憶體重排序型別:

重排序型別 含義
LoadLoad重排序 該重排序指一個處理器上先後執行兩個讀記憶體操作L1和L2,其他處理器對這兩個記憶體操作的感知順序可能是L2——>L1,即L1被重排序到L2之後。
StoreStore重排序 該重排序指一個處理器上先後執行兩個寫記憶體操作W1和W2,其他處理器對這兩個記憶體操作的感知順序可能是W2——>W1,即W1被重排序到W2之後。
LoadStore重排序 該重排序指一個處理器上先後執行讀記憶體操作L1和寫記憶體操作W2,其他處理器對這兩個記憶體操作的感知順序可能是W2——>L1,即L1被重排序到W2之後。
StoreLoad重排序 該重排序指一個處理器上先後執行寫記憶體操作W1和讀記憶體操作L2,其他處理器對這兩個記憶體操作的感知順序可能是L2——>W1,即W1被重排序到L2之後。

記憶體重排序與具體的處理器微架構有關,基於不同微架構的處理器所允許的記憶體重排序是不同的,這裡不再闡述。


重排序可能會導致多執行緒程式出現記憶體可見性問題

  • 對於編譯器,JMM的編譯器重排序規則會禁止特定型別的編譯器重排序

  • 對於處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定型別的記憶體屏障指令,通過記憶體屏障指令來禁止特定型別的處理器重排序。

常見的處理器都不允許對存在資料依賴的操作做重排序

資料依賴性: 如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在資料依賴性。資料依賴分為下列3種型別:
1.寫後讀:a=1;b=a;
2.寫後寫:a=1;a=2;
3.讀後寫:a=b;b=1;

為了遵守as-if-serial語義,編譯器和處理器在重排序時,會遵守資料依賴性,編譯器和處理器不會改變存在資料依賴關係的兩個操作的執行順序。因為這種重排序會改變執行結果。
不同處理器之間和不同執行緒之間的資料依賴性不被編譯器和處理器考慮。

在單執行緒程式中,對存在控制依賴的操作重排序,不會改變執行結果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因);但在多執行緒程式中,對存在控制依賴的操作重排序,可能會改變程式的執行結果。


重排序對多執行緒的影響

image
當操作1和操作2重排序時
image
當操作3和操作4重排序時
image
重排序在這裡破壞了多執行緒程式的語義!

通過加鎖同步可解決該問題
image


為了保證記憶體可見性,Java編譯器在生成指令序列的適當位置會插入記憶體屏障指令來禁止特定型別的處理器重排序

  • 無論是編譯器還是處理器,都需要遵循以下重排序規則:
  1. 臨界區內的操作不允許被重排序到臨界區之外
  2. 臨界區內的操作允許被重排序
  3. 臨界區外的操作之間可以被重排序
  4. 鎖申請與鎖釋放操作不能被重排序
  5. 兩個鎖申請操作不能被重排序
  6. 兩個鎖釋放操作不能被重排序
  7. 臨界區外的操作可以被重排序到臨界區之內

參考資料:
1.Java併發程式設計的藝術(方騰飛 魏鵬 程曉明 著)
2.Java多執行緒程式設計實戰指南(黃文海 著)

相關文章