volatile 示例分析

我被自己蠢哭了(〒︿〒)…
还是要戒骄戒躁, 踏踏实实地学习才行.
“一个人知道自己为何而活, 就可以忍受任何生活” - 尼采.

1 共享变量的内存可见性问题

看这个示例:

public class Test1 {
    private static boolean b = false;

    public static void main(String[] args) throws InterruptedException {
        // 假设这个线程叫 T0
        new Thread(() -> {
            for (; ; ) {
                if (b) {
                    System.out.println(
                            Thread.currentThread().getName() + " " + b);
                    break;
                }
            }
        }).start();

        Thread.sleep(300);
        // 可以用下面的 while 循环代替 Thread.sleep(300),
        // 它俩的消耗的时间差不多
        // long i = 0;
        // while (i++ < 1_000_000_000L) {
        // }

        System.out.println("b 的值为 : " + b);
        b = true;
        System.out.println("修改 b 的值为 : " + b);
    }
}

结果:
result1
程序会一直在 T0 线程 的死循环中, 无法退出. 而且 if (b) 条件无法满足.
尽管 main 线程 已经把 b 的值 修改为 true, 但是 T0 线程 读到的还是 false.

解释:
JMM 规定:
所有的共享变量(实例变量和类变量)都存储于 主内存.
每一个线程还存在自己的 工作内存,线程的 工作内存, 保留了被线程使用的变量的工作副本.
线程对变量的所有读写操作 都必须在 工作内存 中完成, 而不能直接读写 主内存 中的变量.

主内存 和 工作内存 关系如下图(图片来源):
pic1
这就可以解释程序的执行结果了:

  1. T0 线程 开始执行, 进入死循环, 第一次读取 b, 线程的 工作内存 没有, 就去 主内存 复制一份到自己的 工作内存 . 此时 b = false, 程序执行死循环, if 条件 一直不满足.
  2. main 线程 修改 共享变量 b = true 后, 写回到 主内存 .
  3. T0 线程 再次读取 b, 因为 b 在 自己 工作内存 中有值(false), 所以 T0 线程 不会去读取 主内存 的值, 所以 if 条件 任然不满足.
  4. 所以, T0 线程 一直在运行死循环, 无法退出.

如果用 volatile 关键字 修饰 b:

private static volatile boolean b = false;

那么程序的执行结果将是:

b 的值为 : false
修改 b 的值为 : true
Thread-0 true

volatile 保证不同线程对 共享变量 操作的 可见性.
也就是说一个线程修改了 volatile 修饰的变量, 当修改写回 **主内存**时, 其他已经读取的线程的 变量副本置为失效, 其他线程再次读数据时, 因 工作内存 的数据已经失效, 所以它会去 主内存 中读取, 并复制到自己的 工作内存 中.

题外话: volatile 不仅可以保证 内存可见性, 它还可以 禁止指令重排(单例模式-双重检查加锁实现就用到了 volatile 禁止指令重排 的特性).

2 避坑指南

稍微改一下上面的程序:

public class Test1 {
    private static boolean b = false;

    public static void main(String[] args) throws InterruptedException {
        // 假设这个线程叫 T0
        new Thread(() -> {
            for (; ; ) {
                if (b) {
                    System.out.println(
                            Thread.currentThread().getName() + " " + b);
                    break;
                } else {
                    System.out.println("rosie..");
                }
            }
        }).start();

        Thread.sleep(300);
        
        System.out.println("b 的值为 : " + b);
        b = true;
        System.out.println("修改 b 的值为 : " + b);
    }
}

执行结果:

...
(以上省略无数个rosie..)
rosie..
b 的值为 : false
修改 b 的值为 : true
rosie..
Thread-0 true

卧槽! 什么情况?
明明没有用 volatile 修饰共享变量, T0 线程 竟然读到了 主内存 里 更新后的 共享变量.

这是因为 System.out.println 方法 中使用了 synchronized 关键字.

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

synchronized 关键字 也可以保证 内存一致性:
一个线程进入 synchronized代码块 前, 线程会先获得 monitor 锁, 并 清空工作内存, 从 主内存 拷贝 共享变量 最新的值 到 工作内存 成为副本, 然后执行synchronized代码块, 最后将修改后的副本的值刷新回 主内存 中, 线程释放锁.

3 探坑指南

稍微改一下上面的程序:

public class Test1 {
    private static boolean b = false;

    public static void main(String[] args) throws InterruptedException {
        // 假设这个线程叫 T0
        new Thread(() -> {
            for (; ; ) {
                if (b) {
                    System.out.println(
                            Thread.currentThread().getName() + " " + b);
                    break;
                }
                // 只是加了一个 sleep
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        Thread.sleep(300);

        System.out.println("b 的值为 : " + b);
        b = true;
        System.out.println("修改 b 的值为 : " + b);
    }
}

执行结果:

b 的值为 : false
修改 b 的值为 : true
Thread-0 true

可以看到, 没有用 volatile 修饰共享变量, T0 线程 也读到了 主内存 里 更新后的 共享变量.

可以猜测, Thread.sleep 方法 应该也会清除 工作内存, 重新从 主内存 中获取共享变量的值. (纯属猜测, 没有看到相关的文档有介绍)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值