2023-04-08

Java Concurrency #02 - 可見性、原子性、有序性

可見性、原子性和有序性是多線程編程中的三個重要概念,它們是保證多線程程序正確性的三個基本要素。

可見性

在 Multi thread 中,可見性是指當一個 Thread 修改共享變量的值時,該修改對其他 Thread 是否可見的問題。如果修改對其他 Thread 是可見的,則說明該修改是"可見的";如果不可見,則說明該修改是"不可見的"。

在多個 Thread 操作的情況下,任一 Thread 修改變數時,其餘 Thread 可看見修改後的變數

可見性範例

public class Example01 {
    private volatile boolean flag = false;

    public void setFlag() {
        flag = true;
    }

    public void loopUntilFlagIsSet() {
        while (!flag) {
            // do nothing
        }
        System.out.println("Flag is set!");
    }

    public static void main(String[] args) {
        Example01 example = new Example01();

        // start a new thread to loop until flag is set
        new Thread(() -> {
            example.loopUntilFlagIsSet();
        }).start();

        // set the flag after a short delay
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        example.setFlag();
        System.out.println("Flag is now set to true.");
    }
}

在這個範例中,我們有一個 flag 的變數,在 main 執行時,會開啟一個 sub-thread 並且呼叫 loopUntilFlagIsSet 進行 while 等待,過一秒後,我們將 flag 設定為 true,嘗試關閉等待。

若今天沒有宣告 volatile 時,我們將有機會看到 while 迴圈並無跳出,這是因為我們並無法保證 sub-thread 執行時,對於 flag 的可見性,因此變量聲明為 volatile 變數,因此 JVM 保證所有執行緒都能夠及時看到 flag 變數的最新值,從而解決了可見性問題,保證了程序的正確性。

原子性

原子性是指一個操作是不可中斷的,即在該操作執行期間,不會被其他線程干擾。這個操作要麼執行完成,要麼沒有執行。在多線程編程中,原子性通常是使用原子變量來實現的。

在一個操作中,不是全部執行成功,就是全部執行失敗

原子範例

public class Example02 {
    
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        Example02 example = new Example02();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                example.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                example.increment();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(example.getCount());
    }

}

在這個例子中,我們創建了兩個執行緒,分別對 count 進行加1操作。由於 increment() 方法都是 synchronized 方法,因此這兩個執行緒可以安全地對 increment 進行操作,不會出現競爭條件(race condition)等問題。最後,我們獲取 count 值,應該為 20000,表明操作是安全的。

有序性

一般而言,程式碼是從上至下來執行,實則不然,這時候我們就要探討到 Java Model Memory 中的 Reordering 的問題。實際上 JMM 允許編譯器與處理器進行指令重排(Instruction Reorder),來優化執行效率。

當然,這樣的重排不會影響單執行序,這也是為何在併發時,我們需要了解 synchronized、volatile 等方式,來讓我們達成有序性。

在單執行序下,通常不被指令重排影響,但在併發時,我們卻不能保證指令重排後的順序正確性。

結論

本篇文章我們簡單的了解可見性、有序性、原子性三個在併發中不可或缺的概念,並且實作了相關程式,在下一篇章我們將開始研究 Lock 機制該如何使用。

參考

https://chat.openai.com/chat
https://zh.wikipedia.org/zh-tw/%E7%AB%B6%E7%88%AD%E5%8D%B1%E5%AE%B3
https://www.jianshu.com/p/cf57726e77f2
https://medium.com/bucketing/java-concurrency-1-%E5%9C%A8%E9%96%8B%E5%A7%8B%E5%AF%ABcode%E5%89%8D%E5%85%88%E4%BA%86%E8%A7%A3%E4%B8%80%E4%B8%8Bconcurrency%E7%9A%84%E5%9F%BA%E7%A4%8E-8d1a6694eeff
http://jeremymanson.blogspot.com/2007/08/atomicity-visibility-and-ordering.html