首先看下面這段代碼,看起來似乎while循環永遠都不會被異常終止,(tester.x_read, tester.y_read)似乎不會又(0,0)這種結果,但實驗一下會發現,while很高的機率被異常終止,因爲編譯器和執行器在執行代碼指令時,並不會完全按照我們書寫的順序執行,而是通過重排序提高執行效率。重排序後的指令需保證單線程中重排序後語義不變,但多線程中(本例中場景)則無法保證。
public class MemoryModelTester {
int x=0, y=0, x_read, y_read;
Thread createThread1(){
return new Thread(new Runnable() {
@Override
public void run() {
randomSleep();
x=1;
y_read=y;
}
});
}
Thread createThread2(){
return new Thread(new Runnable() {
@Override
public void run() {
randomSleep();
y=1;
x_read=x;
}
});
}
public static void main(String[] args) throws InterruptedException {
while(true) {
MemoryModelTester tester = new MemoryModelTester();
Thread thread1 = tester.createThread1();
Thread thread2 = tester.createThread2();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(String.format("(%d, %d)", tester.x_read, tester.y_read));
if (tester.x_read==0 && tester.y_read==0) {
throw new RuntimeException("stop!");
}
}
}
private void randomSleep() {
try {
Thread.sleep(new Random().nextInt(30));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
什麼時候會發生重排序?爲什麼要進行重排序?
編譯期和運行期都可能發生重排序。
- 編譯器可能會進行指令重排序,所以b變量的賦值操作可能先於a變量。主要通過調整指令順序,在不改變程序語義的前提下,儘可能減少寄存器的讀取、存儲次數,充分複用寄存器的存儲值。
- 處理器可能會對語句所對應的機器指令進行重排序之後再執行,甚至併發地去執行。現代CPU幾乎都採用流水線機制加快指令的處理速度,一般來說,一條指令需要若干個CPU時鐘週期處理,而通過流水線並行執行,可以在同等的時鐘週期內執行若干條指令。
- 內存系統(由高速緩存控制單元組成)可能會對變量所對應的內存單元的寫操作指令進行重排序。重排之後的寫操作可能會對其他的計算/內存操作造成覆蓋。
重排序如何保證單線程中的語意不變?
因爲儘管指令在執行時並不一定按照我們所編寫的順序執行,但Java內存模型通過happens-before原則,保證單線程中重排序後語義不變,指令執行的最終效果與其在順序執行下的效果是一致的。
happens-before原則的具體要求如下:
- unlock發生在lock之前。
- 寫volatile發生在讀volatile之前。
- 線程start()發生在線程所有動作之前。
- 線程中所有操作發生在線程join()之前。
- 構造函數完成發生在finalizer()開始之前。
- 如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C。
如何避免重排序在多線程時造成的影響?
volatile關鍵字可以通過加入內存屏障和禁止重排序來保證變量的可見性,因爲對volatile的操作都在Main Memory中,而Main Memory是被所有線程所共享的,這裏的代價就是犧牲了性能,無法利用寄存器或Cache(因爲它們都不是全局的,無法保證可見性,可能產生髒讀)。
- 對volatile變量寫操作時,會在寫操作後加入一條store屏障指令,將本地內存中的共享變量值刷新到主內存。
- 對volatile變量讀操作時,會在讀操作前加一條load屏障指令,從主內存中讀取共享變量。
需要注意的是:
- 聲明爲volatile字段的作用相當於一個類通過get/set同步方法保護普通字段,但對volatile字段進行“++”操作不會被當做原子操作執行。
- 聲明一個引用變量爲volatile,不能保證通過該引用變量訪問到的非volatile變量的可見性。同理,聲明一個數組變量爲volatile不能確保數組內元素的可見性。
- 當只有一個線程可以修改字段的值,其它線程可以隨時讀取,那麼把字段聲明爲volatile是合理的。
可以使用volatile的情況包括:
- 該字段不遵循其他字段的不變式。
- 對字段的寫操作不依賴於當前值。
- 沒有線程違反預期的語義寫入非法值。
- 讀取操作不依賴於其它非volatile字段的值。
參考:
https://coding.imooc.com/learn/list/195.html
https://www.infoq.cn/article/ftf-java-volatile
https://www.infoq.cn/article/atomic-operation
http://ifeve.com/syn-jmm-pre/
http://ifeve.com/jvm-reordering/