Java內存模型筆記:重排序

首先看下面這段代碼,看起來似乎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);
        }
    }
}
什麼時候會發生重排序?爲什麼要進行重排序?

編譯期和運行期都可能發生重排序。

  1. 編譯器可能會進行指令重排序,所以b變量的賦值操作可能先於a變量。主要通過調整指令順序,在不改變程序語義的前提下,儘可能減少寄存器的讀取、存儲次數,充分複用寄存器的存儲值。
  2. 處理器可能會對語句所對應的機器指令進行重排序之後再執行,甚至併發地去執行。現代CPU幾乎都採用流水線機制加快指令的處理速度,一般來說,一條指令需要若干個CPU時鐘週期處理,而通過流水線並行執行,可以在同等的時鐘週期內執行若干條指令。
  3. 內存系統(由高速緩存控制單元組成)可能會對變量所對應的內存單元的寫操作指令進行重排序。重排之後的寫操作可能會對其他的計算/內存操作造成覆蓋。
重排序如何保證單線程中的語意不變?

因爲儘管指令在執行時並不一定按照我們所編寫的順序執行,但Java內存模型通過happens-before原則,保證單線程中重排序後語義不變,指令執行的最終效果與其在順序執行下的效果是一致的。
happens-before原則的具體要求如下:

  1. unlock發生在lock之前。
  2. 寫volatile發生在讀volatile之前。
  3. 線程start()發生在線程所有動作之前。
  4. 線程中所有操作發生在線程join()之前。
  5. 構造函數完成發生在finalizer()開始之前。
  6. 如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C。
如何避免重排序在多線程時造成的影響?

volatile關鍵字可以通過加入內存屏障和禁止重排序來保證變量的可見性,因爲對volatile的操作都在Main Memory中,而Main Memory是被所有線程所共享的,這裏的代價就是犧牲了性能,無法利用寄存器或Cache(因爲它們都不是全局的,無法保證可見性,可能產生髒讀)。

  1. 對volatile變量寫操作時,會在寫操作後加入一條store屏障指令,將本地內存中的共享變量值刷新到主內存。
    在這裏插入圖片描述
  2. 對volatile變量讀操作時,會在讀操作前加一條load屏障指令,從主內存中讀取共享變量。
    在這裏插入圖片描述

需要注意的是:

  1. 聲明爲volatile字段的作用相當於一個類通過get/set同步方法保護普通字段,但對volatile字段進行“++”操作不會被當做原子操作執行。
  2. 聲明一個引用變量爲volatile,不能保證通過該引用變量訪問到的非volatile變量的可見性。同理,聲明一個數組變量爲volatile不能確保數組內元素的可見性。
  3. 當只有一個線程可以修改字段的值,其它線程可以隨時讀取,那麼把字段聲明爲volatile是合理的。

可以使用volatile的情況包括:

  1. 該字段不遵循其他字段的不變式。
  2. 對字段的寫操作不依賴於當前值。
  3. 沒有線程違反預期的語義寫入非法值。
  4. 讀取操作不依賴於其它非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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章