並發編程基礎底層原理學習(四)
重排序
在程式執行時,為了提高性能,編譯器和處理器常常會對指令做重排序。重排序分為三種類型。
- 編譯器優化的重排序。編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。
- 指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
- 記憶體系統的重排序。由於處理器使用快取和讀/寫緩衝區,使得載入和存儲操作可能是在亂序執行。
重排序可能會導致多執行緒程式出現記憶體可見性問題。指令幾並行重排序和記憶體系統重排序屬於處理器重排序,對於處理器重排序,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的值,由此引發了可見性的問題。
原子性
原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
有序性
重排序都可能會導致多執行緒程式出現記憶體可見性問題