概述
本文大部分整理自《Java并發編程的藝術》,溫故而知新,加深對基礎的理解程度。
指令序列的重排序
我們在編寫代碼的時候,通常自上而下編寫,那么希望執行的順序,理論上也是逐步串行執行,但是為了提高性能,編譯器和處理器常常會對指令做重排序。
1) 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
2) 指令級并行的重排序。現代處理器采用了指令級并行技術來將多條指令重疊執行。如果不存在數據依賴性
,處理器可以改變語句對應機器指令的執行順序。
3) 內存系統的重排序。由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。
從Java源代碼
到最終實際執行的指令序列,會分別經歷下面3種重排序:
happens-before語義
從JDK 5
開始,Java使用新的內存模型,使用happens-before
的概念來闡述操作之間的內存可見性。那到底什么是happens-before
呢?
在JMM中,如果一個操作執行的結果需要對另一個操作可見,那么這兩個操作之間必須要存在
happens-before
關系,這里提到的兩個操作既可以是在一個線程之內,也可以是在不同線程之間。
happens-before規則如下:
程序順序規則:?對于單個線程中的每個操作,前繼操作
happens-before
于該線程中的任意后續操作。
監視器鎖規則:?對一個鎖的解鎖,happens-before
于隨后對這個鎖的加鎖。
volatile變量規則:?對一個volatile域
的寫,happens-before
于任意后續對這個volatile域
的讀。
傳遞性:?如果A happens-before B
,且B happens-before C
,那么A happens-before C
。
注意:
兩個操作之間具有
happens-before關系
,并不意味著前一個操作必須要在后一個操作之前執行,happens-before僅僅要求前一個操作(執行的結果)
對后一個操作可見,且前一個操作按順序排在第二個操作之前。
happens-before與JMM
的關系如圖所示:
如圖所示,一個happens-before
規則對應于一個或多個編譯器和處理器重排序規則。
重排序
重排序指的是:編譯器和處理器為了優化程序性能而對指令序列進行重新排序的一種手段
。
如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作
,此時這兩個操作之間就存在數據依賴性
。數據依賴分為下列3種類型:
上面情況,只要重排序兩個操作的執行順序,程序的執行結果就會被改變
。而編譯器和處理器可能會對操作做重排序,但是編譯器和處理器在重排序時,會遵守數據依賴性
,編譯器和處理器不會改變存在數據依賴關系的兩個操作的執行順序
。
注意:
這里所說的
數據依賴性
僅針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器之間和不同線程之間的數據依賴性不被編譯器和處理器考慮
。
as-if-serial語義
as-if-serial
語義的意思是:不管怎么重排序,單線程程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義
。所以編譯器和處理器不會對存在數據依賴關系
的操作做重排序,因為這種重排序會改變執行結果。但是,如果操作之間不存在數據依賴關系,這些操作就可能被編譯器和處理器重排序。
下面還是以書中的實例(計算圓的面積)進行說明:
double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C
- ?
上面3個操作的數據依賴關系如圖所示:
A和C之間存在數據依賴關系,同時B和C之間也存在數據依賴關系
。因此在最終執行的指令序列中,C不能被重排序到A和B的前面(因為C排到A和B的前面,程序的結果將會被改變
)。但A和B之間沒有數據依賴關系,編譯器和處理器可以重排序A和B之間的執行順序
。
該程序的兩種可能執行順序:
as-if-serial語義
把單線程程序保護了起來,遵守as-if-serial語義
的編譯器、runtime和處理器共同為編寫單線程程序的程序員創建了一個幻覺:單線程程序是按程序的順序來執行的
。
程序順序規則
根據happens-before
的程序順序規則,上面計算圓的面積的示例代碼存在3個happens-before關系
。
1) A happens-before B。
2) B happens-before C。
3) A happens-before C。
而這里的第3個happens-before
關系,是根據happens-before
的傳遞性推導出來的。
注意:
這里
A happens-before B
,但實際執行時B卻可以排在A之前執行,JMM并不要求A一定要在B之前執行。JMM僅僅要求前一個操作(執行的結果)對后一個操作可見,且前一個操作按順序排在第二個操作之前
。這里操作A的執行結果不需要對操作B可見,而且重排序操作A和操作B后的執行結果,與操作A和操作B按happens-before順序執行的結果一致。在這種情況下,JMM會認為這種重排序并不非法,JMM允許這種重排序。
重排序對多線程的影響
重排序是否會改變多線程程序的執行結果?還是借用書中的一個例子:
class ReorderExample {int a = 0;boolean flag = false;public void writer() {a = 1; // 1flag = true; // 2}public void reader() {if (flag) { // 3int i = a * a; // 4}}
}
- ?
flag變量
是個標記,用來標識變量a
是否已被寫入。這里假設有兩個線程A和B
,A首先執行writer()方法,隨后B線程接著執行reader()方法。線程B在執行操作4時,能否看到線程A在操作1對共享變量a的寫入呢?
答案是:不一定能看到
。
由于操作1和操作2沒有數據依賴關系,編譯器和處理器可以對這兩個操作重排序
;同樣,操作3和操作4沒有數據依賴關系,編譯器和處理器也可以對這兩個操作重排序
。
當操作1和操作2重排序時,可能會產生什么效果?(虛箭線標識錯誤的讀操作,用實箭線標識正確的讀操作。
)
如圖所示,操作1和操作2做了重排序
。程序執行時,線程A首先寫標記變量flag,隨后線程B讀這個變量
。由于條件判斷為真,線程B將讀取變量a。此時,變量a還沒有被線程A寫入
,在這里多線程程序的語義被重排序破壞了
!
當操作3和操作4重排序時會產生什么效果。下面是操作3和操作4重排序后,程序執行的時序圖:
在程序中,操作3和操作4存在控制依賴關系
。當代碼中存在控制依賴性時,會影響指令序列執行的并行度
。為此,編譯器和處理器會采用猜測執行
來克服控制相關性對并行度的影響。以處理器的猜測執行為例,執行線程B的處理器可以提前讀取并計算a*a
,然后把計算結果臨時保存到一個名為重排序緩沖的硬件緩存中。當操作3的條件判斷為真時,就把該計算結果寫入變量i
中。猜測執行實質上對操作3和4做了重排序
,在這里重排序破壞了多線程程序的語義
!