多线程-记一次 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~

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值