多線程-記一次 volatile 實驗出錯所得

微信搜索 程序員的起飛之路 可以加我公衆號,保證一有乾貨就更新~
二維碼如下:
公衆號

好,進入正題,今日學習 volatile 時,偶然想起之前見過的一段代碼,正好說明了 volatile 的可見性,而我寫博客也正好用的上。於是打算手擼一版出來,就有了下面的版本:

public class VolatileTest {
    static class Test {
        public volatile boolean flag = false;
    }

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

        new Thread(() -> {
            try {
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            test.flag = true;
        }).start();

        while (true) {
            if (test.flag) {
                System.out.println("flag changed!");
                break;
            } else {
                System.out.println("flag not changed!");
            } 
            TimeUnit.SECONDS.sleep(1L);
        }
    }
}

然後我滿心歡喜的執行了,希望出現如下效果:
期望執行結果
但是!!!結果並不如我所願!!竟然出現了這種結果:
實際執行結果
WTF!!!我直接就爆了粗口。冷靜下來仔細攻研代碼,覺得沒問題啊!一個線程讀,另一個線程改。改的是工作副本中的數據,這裏不應該能讀到啊。這要是能讀到我還要 volatile 幹嘛!於是我默默的去百度別人家的代碼,找到了如下代碼:

public class VolatileDemo {
    public static void main(String[] args) throws InterruptedException {
        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();

        while (true) {
            if (td.flag) {
                System.out.println("flag changed");
                break;
            }
        }
    }

    static class ThreadDemo implements Runnable {
        boolean flag = false;

        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(2L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
            System.out.println("flag = " + flag);
        }
    }
}

這段代碼跟我的沒啥區別啊,只不過我新建的是對象,他新建的是個 Runnable 對象罷了,見鬼的是這個代碼段執行結果是預期之中的 變量不可見導致循環未停止 現象!WTF???什麼鬼!!我頓時傻眼,就拿這兩段代碼請教技術羣的各位大佬。羣裏各位大佬出謀劃策獻計紛紛。最終我的代碼被改成了如下:

public class VolatileTest {
    static class Test {
        public boolean flag = false;
    }

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

        new Thread(() -> {
            try {
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            test.flag = true;
        }).start();

        while (!test.flag) {
        }

        System.out.println("flag changed!");

    }
}

這樣確實實現了我想要的效果:循環空轉不停,flag changed! 未被打印。但是我還是不明白啊!到底我的代碼哪裏出了問題。這時一位名爲 @顏如玉 的大佬跟我講,System.out.println 中是有 synchronize 鎖的,而 synchronize 對一個變量執行unlock操作之前,必須先把此變量同步回主內存中(執行 store、write 操作)。隨後我在偉大的併發編程網上也找到了答案,文章是《同步和Java內存模型 (三)可見性》,答案如下:

一個寫線程釋放一個鎖之後,另一個讀線程隨後獲取了同一個鎖。本質上,線程釋放鎖時會將強制刷新工作內存中的髒數據到主內存中,獲取一個鎖將強制線程裝載(或重新裝載)字段的值。鎖提供對一個同步方法或塊的互斥性執行,線程執行獲取鎖和釋放鎖時,所有對字段的訪問的內存效果都是已定義的。

原文:A writing thread releases a synchronization lock and a reading thread subsequently acquires that same synchronization lock.
In essence, releasing a lock forces a flush of all writes from working memory employed by the thread, and acquiring a lock forces a (re)load of the values of accessible fields. While lock actions provide exclusion only for the operations performed within a synchronized method or block, these memory effects are defined to cover all fields used by the thread performing the action.

然後我在改完後的代碼上加了一行 System.out.println(),雖然什麼都沒輸出,但還是破壞了不可見現象。於是我返回我寫的源代碼,將 sout 刪掉,改後代碼如下:

public class VolatileTest {
    static class Test {
        public boolean flag = false;
    }

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

        new Thread(() -> {
            try {
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            test.flag = true;
        }).start();

        while (true) {
            if (test.flag) {
                System.out.println("flag changed!");
                break;
            } 
            TimeUnit.SECONDS.sleep(1L);
        }
    }
}

然後開開心心的跑起來。嗯。不出你們所料。又特麼失敗了!!!又雙叒叕!!我這次直接放棄掙扎了,果斷甩代碼進羣。求大佬們幫助。結果當時在線的大佬們也是一籌莫展,表示一臉懵逼。那我就自己來,對比成功代碼和失敗代碼以後發現,多了 TimeUnit.SECONDS.sleep(1L); 一句話。果斷幹掉,發現成了!!拿着結論去羣裏問大佬。有大佬就明白了,@我GTR就不服AE86 大佬說明了一下,sleep 0 觸發了線程重新競爭cpu,線程要保存當前上下文才會釋放cpu ,而保存上下文則將變量刷入了主內存,至此全部搞懂。改變 volatile 後也成功將兩種現象復現。最終代碼如下:

public class VolatileTest {
    static class Test {
        public boolean flag = false;
    }

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

        new Thread(() -> {
            try {
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            test.flag = true;
        }).start();

        while (true) {
            if (test.flag) {
                System.out.println("flag changed!");
                break;
            }
        }
    }
}

讀書越多越發現自己的無知,Keep Fighting!

歡迎友善交流,不喜勿噴~
Hope can help~

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