code

2016年12月3日 星期六

Java Concurrency 4 - 小心compiler optimization造成的stale data!

奇怪的問題

以下的程式碼,我在工作中也是這樣寫,邏輯上似乎沒有問題才對,沒想到讀了這段文章之後,才知道我可能寫錯了!!!

public class NoVisibility {
    private static boolean ready;
    private static int number;
    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }
    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    } 
}

錯的原因在於ReaderThread不見得看得到ready和number這兩個變數的改變!為什麼會這樣?

ReaderThread可能發生情況包括:(1) 無窮迴圈 (2) 印出number為0 (3)按照我們的期望執行印出42

(1)(2)發生的原因難以理解,因為這是我們programmer看不到的邏輯:compiler optimization造成的execution reordering,因為對compiler來說某些指令如果沒有dependency的話,先後順序可以不按照程式行碼的順序來編排,這是為了多核心CPU所做的最佳化。

所以在main method中,創造新的ReaderThread, 寫入number = 42 和 寫入 ready = true這三行順序可能被compiler重新安排順序,因為兩者沒有關聯性,所以我們依照這個順序性成立的假設來實作ReaderThread的run method是錯誤的假設!

不過不理解為什麼會無窮迴圈?因為ready最終會被設為true,好奇怪?按照書中說法,當讀寫shared variable時候,其他thread不保證會看到ready = true被寫入這件事,書中提到可能是因為CPU cache造成ReaderThread讀到outdated ready value,這倒是說得通,不過這樣的optimization也未免太嚇人,multithreading的時候要注意optimization造成的side effect。


我個人在桌機執行上面的程式,儘管開了5條ReaderThread也是印出42,所以可能跟硬體和JVM compiler的設定有關。

不求甚解的我,conclude書中的話好了:反正有被multithreads shared的variables,都請用synchronization保護!

Stale data

上面例子說到ReaderThread可能看到非預期的變數值,因為compiler optimization造成的,這種過期(有時候混合正確的值)的資料稱為stale data,非常難以用邏輯理解(因為我們的邏輯需要建設在某些假設上,但這些假設在compiler optimization之後就不成立)。

重點在“讀取”shared variables也一定要synchronized保護,否則stale data是可能發生的。只有這樣能保證不同thread讀到最新的值。

一個可能發生的怪現象是JVM允許implementation把64 bit non-volatile long/double 用兩個32 bit read/write指令來完成指令來完成 原本應該是atomic operation的fetch and store operation,所以也有可能發生upper 或 lower 32 bits是stale value,相當恐怖,這一般應該查不出原因,如果不知道這麼眉角的話。

Java提供了volatile keyword,告訴compiler這個variable是shared,不要做reordering,可以解決這個問題,但是並沒有synchronization的保護。


沒有留言:

張貼留言