jmmIOVisibility

shakerChann發表於2024-07-23

IO操作引起的記憶體可見性

問題

@Data
public class ChangeThread implements Runnable {
    /**
     * volatile
     **/
    boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("subThread change flag to:" + flag);
        flag = true;
        System.out.println("subThread change flag to:" + flag);
    }
}
    public static void main(String[] args) {
        ChangeThread changeThread = new ChangeThread();
        new Thread(changeThread).start();
        while (true) {
            if (changeThread.isFlag()) {
                System.out.println("detected flag changed");
                break;
            }else {
//                System.out.println("false flag ...");
            }
        }
        System.out.println("main thread end");
    }

程式碼塊二中:main函式的else中System.out.println是否執行對結果的影響?

結論:

執行System.out.println,主執行緒會按照預期退出;

不執行System.out.println的話,主執行緒會一直執行。

從結果上看,System.out.println 導致flag的值,從主記憶體(強制?)重新整理到main執行緒的工作記憶體;列印語句被註釋掉時,mainThread 可能會不斷地在自己的快取中讀取 flag 的值,而不是從主記憶體中讀取。如果 flag 的值沒有被修改,這種讀取可能會一直讀取到舊值,從而導致 mainThread 進入一個無限迴圈。

分析:

一、System.out.println 屬於輸入/輸出(I/O)操作中的輸出操作。它將資料輸出到控制檯,這涉及與作業系統的互動來處理輸出裝置(通常是顯示器)。列印操作引發 I/O 操作,I/O 操作通常是緩慢的,會導致執行緒的狀態發生變化。這裡嘗試其他io操作(log等)是同樣的效果。

I/O 操作的副作用:I/O 操作(如列印)通常會涉及底層的系統呼叫,可能會導致執行緒排程器重新排程執行緒。這種排程可能會導致 mainThread 從主記憶體中重新載入變數,從而看到最新的值。

最佳化行為:JVM 和 CPU 的最佳化行為可能在某些情況下使得列印操作重新整理了快取行或工作記憶體,但這種行為並不可靠或可預見。

二、System.out 是一個 PrintStream 物件,而 PrintStream 的寫操作通常會呼叫內部的同步方法。每次呼叫 println 時,都會獲得 PrintStream 物件的鎖,並在釋放鎖之前執行寫操作。鎖機制會確保記憶體的可見性:當一個執行緒釋放一個鎖時,Java 記憶體模型會確保這個執行緒對共享變數的修改對隨後獲得這個鎖的執行緒可見。

但是,獲取和釋放 PrintStream 的鎖與 flag 的記憶體可見性之間,應該並沒有直接關係。

解決:

1.volatile 修飾flag變數,保證記憶體可見性

2.unsafe.loadFence() ,保證在這個屏障之前的所有讀操作都已經完成,並且將快取資料設為無效,重新從主存中進行載入。

延申:

一、jmm 什麼清空下會觸發,重新載入主存中資料

在 Java 記憶體模型(Java Memory Model, JMM)中,重新載入主存中資料通常發生在以下幾種情況下:

  1. 執行緒啟動
    • 當一個執行緒啟動時,它會清空其工作記憶體(執行緒的本地快取)中的資料,並從主存中重新載入所有共享變數的最新值。
  2. volatile 變數
    • volatile 變數的讀寫操作會導致執行緒直接從主存中讀取資料或將資料寫回主存。具體來說,當一個執行緒讀取 volatile 變數時,它會強制從主存中讀取最新的值;當一個執行緒寫入 volatile 變數時,它會將新值立即重新整理到主存。
  3. 鎖和解鎖操作
    • 執行緒在執行 synchronized 塊或方法時,當它獲取鎖時,會清空工作記憶體,並從主存中重新載入被監視物件的所有共享變數。當釋放鎖時,會將工作記憶體中對這些變數的修改重新整理到主存。
    • 這也適用於 Lock 介面及其實現,例如 ReentrantLock
  4. final 變數
    • 對於 final 變數,在物件構造器內的賦值操作會確保物件構造完畢且構造器完成後,final 變數的值對所有其他執行緒可見。
  5. 執行緒終止
    • 當一個執行緒終止時,所有對共享變數的修改都會被重新整理到主存,確保其他執行緒可以看到這些修改。

二:記憶體間互動操作(深入理解jvm)

·lock(鎖定):作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態。

·unlock(解鎖):作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。

·read(讀取):作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用。

·load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。

·use(使用):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。

·assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。

·store(儲存):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用。

·write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中。

如果要把一個變數從主記憶體複製到工作記憶體,那就要按順序執行read和load操作,如果要把變數從工作記憶體同步回主記憶體,就要按順序執行store和write操作。注意,Java記憶體模型只要求上述兩個操作必須按(相對)順序執行,但不要求是連續執行。也就是說read與load之間、store與write之間是可插入其他指令的。