並發編程基礎底層原理學習(四)

重排序

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

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

重排序可能會導致多執行緒程式出現記憶體可見性問題。指令幾並行重排序和記憶體系統重排序屬於處理器重排序,對於處理器重排序,JMM會要求Java編譯器在生成指令時,插入特定的記憶體屏障指令,通過記憶體屏障指令來禁止特定類型的處理器重排序。

記憶體屏障類型
屏障類型 指令示例 說明
LoadLoad Barriers Load1; LoadLoad; Load2 確保Load1數據的裝載先於Load2及所有後續裝載指令的裝載
Store Store Barriers Store1; StoreStore;Store2; 確保Store1數據對其他處理器可見先於Store2及所有後續的存儲指令的存儲
LoadStore Barriers Store1; StoreLoad; Load2 確保Store1數據裝載先於Store2及所有後續的存儲指令刷新到記憶體
StoreLoad Barriers Store1; StoreLoad; Load2 確保Store1數據對其他處理器變得可見(指刷新到記憶體)先於Load2及所有後續裝載指令的裝載。
happen-before

從JDK5開始,Java使用JSR-133記憶體模型,JSR-133使用happen-before的概念來闡述操作之間的記憶體可見性。在JMM中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須存在happen-before關係。

Happen-before規則如下:

  • 程式順序規則:一個執行緒中的每個操作,happen-before於該執行緒中的任意後續操作。
  • 監視器鎖規則:對一個鎖的解鎖,happen-before於隨後對這個鎖的加鎖。
  • volatile變數規則:對一個volatile 域的寫,happen-before於任意後續對這個volatile域的讀
  • 傳遞性:如果A happen-before B,且B happen-before C,那麼 A happen-before C。
引發並發安全的三大因素
可見性

可見性:一個執行緒對共享變數的修改,另外一個執行緒能夠立刻看到。

// 執行緒1
int a = 0;
a = 1;
// 執行緒2
int b = a;

假如執行緒1由cpu1執行,執行緒2由cpu2執行,當執行a=1時,會把a的初始值0載入到cpu1的高速快取中,然後賦值為1,此時cpu1高速快取中a的值變成了1,但是還沒有立即刷新到主記憶體。此時cpu2執行b=a時會從主記憶體中讀取a的值然後載入到cpu2的高速快取,此時a在主記憶體的值仍然是0,那麼b的值此時為0而不是1。這就是可見性問題,執行緒1對變數i修改了之後,執行緒2沒有立即看到執行緒1修改的值。執行緒1對共享變數a進行修改之後,執行緒2沒有立即看到修改之後a的值,由此引發了可見性的問題。

原子性

原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。

有序性

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