从一段demo思考volatile是如何解决可见性的

Volatile

volatile案例

下面是一个刚接触volatile关键字时,关于volatile的一个demo。

主要逻辑为:先启动一个子线程t1,其中循环直至flag不为true,而主线程sleep一段时间后修改flag为true,观察到子线程是否停止。

public class VolatileDemo {
    static boolean flag = true;
    static long i = 0;
    public static void main(String[] args) {
        
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t-------come in");
            long startTime = System.currentTimeMillis();
            while (flag) {
                i++;
            }
            System.out.println(i);
            long endTime = System.currentTimeMillis();
            System.out.println("costTime:" + (endTime - startTime) + " 毫秒");
            System.out.println(Thread.currentThread().getName() + "\t-------flag被设置为false,程序停止");
        }, "t1").start();

        try {
            TimeUnit.MILLISECONDS.sleep(20);
//            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //更新flag值
        flag = false;
        System.out.println(Thread.currentThread().getName() + "\t 修改完成");
    }
}

​ 当flag没有用volatile修饰时,子线程可能会察觉不到主线程修改了flag。

​ 同时,在和一些同学交流后,我们发现,子线程能否察觉flag的变动与主线程的sleep时间相关,当休眠时间为2ms时,主线程对flag的操作能及时同步到子线程中使得子线程停止,而休眠时间较长时,子线程无法察觉到flag的变动。

​ 那么如何解释这种现象呢?

现象解释

JIT?

​ 有同学提出这可能是因为JIT(JAVA即时编译),子线程长时间执行相同逻辑,即循环判断flag,导致JVM对其进行了优化,导致其一直察觉不到flag的变化。

​ 我们首先来了解一下JIT技术。当JVM发现某个方法或代码块运行特别频繁的时候,会认为这是”热点代码“(Hot Spot Code)。然后JIT会把部分”热点代码“编译成本地机器相关的机器码,并进行优化,然后再把编译后的机器码缓存起来,以备下次使用。

​ 从概念上面理解,这里JIT仅仅只是将字节码转换为机器码,避免了每次都需要将字节码转换为机器码这个过程,并没有解释为什么子线程的缓存没有再次从主线程中取数据。解释有些牵强,下面禁用JIT查看子线程能否停止。

image-20240424110308502

​ 但是,当我们禁用JIT之后,子线程依然不会停止。那么显然当前的解释是错误的。

MESI协议

​ MESI缓存一致性协议,是一种保证多核CPU的缓存之间的一致性协议。简单来说,如果一个变量为多个核心所有(持有该变量的线程位于多个核心上),即处于S(Share共享)状态。此时有一个线程修改了该变量的值,主存通过嗅探机制察觉到变量发生了变化,然后MESI协议会在一定的时间内通知其他线程该变量发生了变动,将当前对应的变量转变为I(Invalidated失效)状态,此时,其他线程将重新从主存中读取变量。

​ 通过上面的描述,我们可以发现,MESI协议能够在一定的时间内通知各个相关线程,并不会出现永久无法察觉共享变量变化的情况。

while (flag) {
    i++;
}

​ 重新思考一下子线程的执行逻辑,我们可以做出如下推测:

​ 这段代码是将flag先从内存加载到CPU cache,然后通过MOV指令将flag变量放入对应的寄存器中,因为循环中涉及到的变量比较少,flag变量此后一直存在于寄存器中,无需重新从cpu cache取数据,以至于当前线程一直认为flag是保存在寄存器中的true值,继而无法停止。(当然这个推断不一定对,仅仅是个人思考,关于重排序的一些原理还没有涉及到,不太懂)

使用Volatile

使用volatile可以保证两大特性,可见性与有序性

可见性

​ 可见性是通过内存屏障实现的,内存屏障,又称内存栅栏,是一个CPU指令。

  1. 阻止屏障两边的指令重排序

  2. 写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存

    1. volatile写之前加入一个storestore屏障,禁止上面的普通写和下面的volatile写重排序,保证前面普通写的数据全部刷新到主内存中

      普通写和volatile写禁止重排,volatile写和volatile写禁止重排

    2. volatile写之后加入storeload屏障,禁止上面的volatile写和下面的volatile读写或普通写重排序,保证前面volatile写的操作,数据都已经全部刷新到主内存

      volatile写和普通写禁止重排,volatile写和volatile读/写禁止重排

  3. 读数据时加入屏障,线程私有工作内存数据失效,重新回到主物理内存获取最新数据

​ 显然,使用volatile之后,能够保证volatile读会重新从主存中取数据,就不会出现像上面一样的问题了。

有序性

​ happens-before 规则中有一条是 volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

  • 28
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值