天天看點

java記憶體模型——重排序

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

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

在執行程式時,為了提高性能,編譯器和處理器常常會對指令做重排序。重排序分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語義允許對存在控制依賴的操作做重排序的原因);但在多線程程式中,對存在控制依賴的操作重排序,可能會改變程式的執行結果。

重排序對多線程的影響

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

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

繼續閱讀