高并發編程-重排序
文章目錄
定義
數據依賴性
as-if-serial語義
程序順序規則
重排序對多線程的影響
定義
重排序是指編譯器和處理器為了優化程序性能而對指令序列進行重新排序的一種手段。
數據依賴性
如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據依賴性.
上面3種情況,只要重排序兩個操作的執行順序,程序的執行結果就會被改變。
前面提到過,
編譯器和處理器可能會對操作做重排序。編譯器和處理器在重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關系的兩個操作的執行順序。
這里所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器之間和不同線程之間的數據依賴性不被編譯器和處理器考慮。
as-if-serial語義
as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執行結果不能被改變。
編譯器、runtime和處理器都必須遵守as-if-serial語義。
為了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關系的操作做重排序,因為這種重排序會改變執行結果。
但是,如果操作之間不存在數據依賴關系,這些操作就可能被編譯器和處理器重排序。
舉個例子 : 計算圓面積
double pi = 3.14; // A double r = 1.0; // B double area = pi * r * r; // C
1
2
3
上面3個操作的數據依賴關系如下所示
A和C之間存在數據依賴關系,同時B和C之間也存在數據依賴關系。因此最終執行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的結果將會被改變)。
但A和B之間沒有數據依賴關系,編譯器和處理器可以重排序A和B之間的執行順序。
as-if-serial語義把單線程程序保護了起來,遵守as-if-serial語義的編譯器、runtime和處理器共同為編寫單線程程序的程序員創建了一個幻覺:單線程程序是按程序的順序來執行的。as-if-serial語義使單線程程序員無需擔心重排序會干擾他們,也無需擔心內存可見性問題。
程序順序規則
根據happens-before的程序順序規則,上面計算圓的面積的示例代碼存在3個happens-before關系。
1)A happens-before B。 2)B happens-before C。 3)A happens-before C。
1
2
3
這里的第3個happens-before關系,是根據happens-before的傳遞性推導出來的。
重排序對多線程的影響
我們來看看,重排序是否會改變多線程程序的執行結果。 請看下面的示例代碼
public class AsIfSerial { private int a = 0; private boolean flag = false; public void wirte() { a = 1; // 操作1 flag = true;// 操作2 System.out.println(Thread.currentThread().getName() + " 更新后 a=" + a + " , flag=" + flag); } public void read() { System.out.println(Thread.currentThread().getName() + " 讀取值 a=" + a + " , flag=" + flag); if (flag) { // 操作3 int i = a * a; // 操作4 System.out.println(Thread.currentThread().getName() + " 執行結果:" + i); } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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存在控制依賴關系。
當代碼中存在控制依賴性時,會影響指令序列執行的并行度。
為此,編譯器和處理器會采用猜測(Speculation)執行來克服控制相關性對并行度的影響。以處理器的猜測執行為例,執行線程B的處理器可以提前讀取并計算a*a,然后把計算結果臨時保存到一個名為重排序緩沖(Reorder Buffer,ROB)的硬件緩存中。當操作3的條件判斷為真時,就把該計算結果寫入變量i中。
從上圖中我們可以看出,猜測執行實質上對操作3和4做了重排序。重排序在這里破壞了多線程程序的語義!
在單線程程序中,對存在控制依賴的操作重排序,不會改變執行結果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因);但在多線程程序中,對存在控制依賴的操作重排序,可能會改變程序的執行結果。
咋改呢 ? 加 volatile
package com.artisan.test; public class AsIfSerial { // 共享變量: 實例域 (同一個對象的) private volatile int a = 0; // 共享變量: 實例域(同一個對象的) private volatile boolean flag = false; public void wirte() { a = 1; flag = true; System.out.println(Thread.currentThread().getName() + " 更新后 a=" + a + " , flag=" + flag); } public void read() { System.out.println(Thread.currentThread().getName() + " 讀取值 a=" + a + " , flag=" + flag); if (flag) { int i = a * a; System.out.println(Thread.currentThread().getName() + " 執行結果:" + i); } } public static void main(String[] args) throws InterruptedException { // 實例化出來一個對象 AsIfSerial asIfSerial = new AsIfSerial(); new Thread(() -> { asIfSerial.wirte(); }, "WRITE").start(); // sleep一下 確保 WRITE線程先啟動 Thread.sleep(500); new Thread(() -> { asIfSerial.read(); }, "READ").start(); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
任務調度 多線程
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。