個人博客請訪問 http://www.x0100.top
併發編程的三大問題:原子性、可見性、有序性。
緩存不能及時刷新導致了可見性問題。
編譯器爲了優化性能而改變程序中語句的先後順序,導致有序性問題。
而“緩存不能及時刷新“和“編譯器爲了優化性能而改變程序中語句的先後順序”都是重排序的一種。
1. 重排序概念
在執行程序時爲了提高性能,編譯器和處理器常常會對指令做重排序。
從 java 源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:
-
編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
-
指令級並行的重排序。處理器將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
-
內存系統的重排序。處理器使用緩存和讀/寫緩衝區,使得加載和存儲操作看上去可能是在亂序執行。
舉例:如下代碼執行過程中,程序不一定按照先A後B的順序執行,經重排序之後可能按照先B後A的順序執行。
int a = 1;// A
int b = 2;// B
2. 重排序規則
重排序需要遵守一定規則,以保證程序正確執行。
重排序遵守數據依賴性
數據依賴性:如果兩個操作訪問同一個變量,且這兩個操作中有一個爲寫操作,此時這兩個操作之間就存在數據依賴性。
存在數據依賴性的三種情況:
① 寫後讀:a = 1;b = a; 寫一個變量之後,再讀這個位置。
② 寫後寫:a = 1;a = 2; 寫一個變量之後,再寫這個變量。
③ 讀後寫:a = b;b = 1;讀一個變量之後,再寫這個變量。
存在數據依賴關係的兩個操作,不可以重排序。
數據依賴性只針對單個處理器中執行的指令序列和單個線程中執行的操作。
舉例:
同一個線程中執行a=1;b=1; 不存在數據依賴性,可能重排序。
同一個線程中執行a=1;b=a; 存在數據依賴性,不可以重排序。
重排序遵守as-if-serial 語義
as-if-serial 語義:不管怎麼重排序(編譯器和處理器爲了提高並行度),(單線程)程序的執行結果不能被改變。
舉例,以計算圓的面積爲例:
double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C
A和B重排序之後,程序的執行結果不會改變,所以允許A、B重排序。A和C重排序之後,程序的執行結果會改變,所以不允許A、C重排序。
筆者看來,遵守數據依賴性和as-if-serial 語義實質上是一回事。爲了遵守 as-if-serial 語義,編譯器和處理器不會對存在數據依賴關係的操作做重排序,因爲這種重排序會改變執行結果。
3. 重排序帶來的問題
重排序可以提高程序執行的性能,但是代碼的執行順序改變,可能會導致多線程程序出現可見性問題和有序性問題。
舉例:
初始狀態:a = b = 0;x = y = 0;
Processor A:
a = 1; // A1
x = b; // A2
Processor B:
b = 2; // B1
y = a; // B2
如上代碼,Processor A和Processor B同時執行,最終卻可能得到x = y = 0的結果。
原因分析:
第一步執行A1/B1將a=1寫到緩衝區,此時寫緩衝區還在等待其他寫操作,不執行A3,所以內存中的a=0;
第二步執行A2/B2,處理器讀取內存中的a,得到a=0;
雖然處理器 A 執行內存操作的順序爲:A1->A2,但內存操作實際發生的順序卻是:A2->A1。此時,處理器 A 的內存操作順序被重排序了。
4. JMM如何解決重排序問題
JMM處理重排序問題:
1)對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。
2)對於處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障指令,來禁止特定類型的處理器重排序。
3)JMM根據代碼中的關鍵字(如:synchronized、volatile)和J.U.C包下的一些具體類來插入內存屏障。
JMM 把內存屏障指令分爲下列四類:
Store:數據對其他處理器可見(即:刷新到內存中)
Load:讓緩存中的數據失效,重新從主內存加載數據
總結
在執行程序時爲了提高性能,編譯器和處理器常常會對指令做重排序。
從Java源代碼到最終實際執行,要經歷三種重排序:編譯器優化的重排序、指令級並行的重排序、內存系統的重排序。
as-if-serial語義要求:不管怎麼重排序,程序的執行結果不能被改變。
存在數據依賴關係的兩個操作,不可以重排序。
重排序可能會導致多線程程序出現可見性問題和有序性問題。
JMM編譯時在當位置會插入內存屏障指令來禁止特定類型的重排序。