高并发编程-重排序
文章目錄
- 定義
- 數(shù)據(jù)依賴性
- as-if-serial語義
- 程序順序規(guī)則
- 重排序?qū)Χ嗑€程的影響
定義
重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序的一種手段。
數(shù)據(jù)依賴性
如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數(shù)據(jù)依賴性.
| 寫后讀 | a=1;b=a; | 寫一個變量后,再讀這個位置 |
| 寫后寫 | a=1;a=2 | 寫一個變量后,再寫這個變量 |
| 讀后寫 | a=b;b=1; | 讀一個變量后,再寫這個變量 |
上面3種情況,只要重排序兩個操作的執(zhí)行順序,程序的執(zhí)行結(jié)果就會被改變。
前面提到過,編譯器和處理器可能會對操作做重排序。編譯器和處理器在重排序時,會遵守數(shù)據(jù)依賴性,編譯器和處理器不會改變存在數(shù)據(jù)依賴關(guān)系的兩個操作的執(zhí)行順序。
這里所說的數(shù)據(jù)依賴性僅針對單個處理器中執(zhí)行的指令序列和單個線程中執(zhí)行的操作,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮。
as-if-serial語義
as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。
編譯器、runtime和處理器都必須遵守as-if-serial語義。
為了遵守as-if-serial語義,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因為這種重排序會改變執(zhí)行結(jié)果。
但是,如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序。
舉個例子 : 計算圓面積
double pi = 3.14; // A double r = 1.0; // B double area = pi * r * r; // C上面3個操作的數(shù)據(jù)依賴關(guān)系如下所示
A和C之間存在數(shù)據(jù)依賴關(guān)系,同時B和C之間也存在數(shù)據(jù)依賴關(guān)系。因此最終執(zhí)行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的結(jié)果將會被改變)。
但A和B之間沒有數(shù)據(jù)依賴關(guān)系,編譯器和處理器可以重排序A和B之間的執(zhí)行順序。
as-if-serial語義把單線程程序保護了起來,遵守as-if-serial語義的編譯器、runtime和處理器共同為編寫單線程程序的程序員創(chuàng)建了一個幻覺:單線程程序是按程序的順序來執(zhí)行的。as-if-serial語義使單線程程序員無需擔(dān)心重排序會干擾他們,也無需擔(dān)心內(nèi)存可見性問題。
程序順序規(guī)則
根據(jù)happens-before的程序順序規(guī)則,上面計算圓的面積的示例代碼存在3個happens-before關(guān)系。
1)A happens-before B。 2)B happens-before C。 3)A happens-before C。這里的第3個happens-before關(guān)系,是根據(jù)happens-before的傳遞性推導(dǎo)出來的。
重排序?qū)Χ嗑€程的影響
我們來看看,重排序是否會改變多線程程序的執(zhí)行結(jié)果。 請看下面的示例代碼
public class AsIfSerial {private int a = 0;private boolean flag = false;public void wirte() {a = 1; // 操作1flag = true;// 操作2System.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; // 操作4System.out.println(Thread.currentThread().getName() + " 執(zhí)行結(jié)果:" + i);}}}flag變量是個標記,用來標識變量a是否已被寫入。這里假設(shè)有兩個線程A和B,A首先執(zhí)行writer()方法,隨后B線程接著執(zhí)行reader()方法。線程B在執(zhí)行操作4時,能否看到線程A在操作1對共享變量a的寫入呢? ---------->不一定能看到.
由于操作1和操作2沒有數(shù)據(jù)依賴關(guān)系,編譯器和處理器可以對這兩個操作重排序;同樣操作3和操作4沒有數(shù)據(jù)依賴關(guān)系,編譯器和處理器也可以對這兩個操作重排序。
讓我們先來看看,當操作1和操作2重排序時,可能會產(chǎn)生什么效果?
操作1和操作2做了重排序。程序執(zhí)行時,線程A首先寫標記變量flag,隨后線程B讀這個變量。由于條件判斷為真,線程B將讀取變量a。此時,變量a還沒有被線程A寫入,在這里多線程程序的語義被重排序破壞了! (虛箭線標識錯誤的讀操作)
再讓我們看看,當操作3和操作4重排序時會產(chǎn)生什么效果(借助這個重排序,可以順便說明控制依賴性)。下面是操作3和操作4重排序后,程序執(zhí)行的時序圖
在程序中,操作3和操作4存在控制依賴關(guān)系。當代碼中存在控制依賴性時,會影響指令序列執(zhí)行的并行度。
為此,編譯器和處理器會采用猜測(Speculation)執(zhí)行來克服控制相關(guān)性對并行度的影響。以處理器的猜測執(zhí)行為例,執(zhí)行線程B的處理器可以提前讀取并計算a*a,然后把計算結(jié)果臨時保存到一個名為重排序緩沖(Reorder Buffer,ROB)的硬件緩存中。當操作3的條件判斷為真時,就把該計算結(jié)果寫入變量i中。
從上圖中我們可以看出,猜測執(zhí)行實質(zhì)上對操作3和4做了重排序。重排序在這里破壞了多線程程序的語義!
在單線程程序中,對存在控制依賴的操作重排序,不會改變執(zhí)行結(jié)果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因);但在多線程程序中,對存在控制依賴的操作重排序,可能會改變程序的執(zhí)行結(jié)果。
咋改呢 ? 加 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() + " 執(zhí)行結(jié)果:" + 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();}}總結(jié)
- 上一篇: 高并发编程-happens-before
- 下一篇: RocketMQ-初体验RocketMQ