前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >你连volatile都不在意,你在意什么,在意大利吗

你连volatile都不在意,你在意什么,在意大利吗

作者头像
用户7386338
发布2020-07-16 15:52:58
2420
发布2020-07-16 15:52:58
举报
文章被收录于专栏:Java患者Java患者

深入理解Volatile

初步认识

public class ThreadDemo1 {
    public  static boolean stop = false;

    public static void main(String[] args)  {
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true;
            System.out.println("stop 修改为 true");
        }).start();

        while (true) {
            if (stop) {
                System.out.println(" stop 变为 true");
                break;
            }
        }

    }
}

当我们执行这段代码的时候,我们的预期是1秒之后会执行 "stop 变为 true",但是我们的输出结果一直是"stop 修改为true", 既然修改为了true, 那么不就会执行while的代码吗?

接着使用volatile修饰了静态变量stop,达到了预期的效果,这就是volatile的作用了。

不信你看

volatile的作用

volatile可以使在多处理器环境下保证了共享变量的可见性,什么是可见性,通俗来讲:在一个单线程的环境下,如果向一个变量stop先写入一个值,然后在没有写干涉的情况下读取这个变量stop的值,那么这个时候读取到的这个变量应该是你之前写入的那个值,这是很正常的,但是在多线程下,读和写发生在不同线程的时候,就有可能会出现:读线程不能及时的读取到其他线程写入的最新的值。这就是可见性。这个时候我们必须使用某种机制来实现跨线程写入的内存的可见性,而volatile就是这种机制。

volatile的原理是什么

当我们的代码被编译会汇编指令后,我们可以发现在带有volatile修饰的成员变量时,会多一个lock指令,lock指令时一种控制指令,在多处理器的环境下,lock指令可以基于总线锁或者是缓存锁的机制来达到一个可见性的效果。

可见性的本质

我们要从计算机的核心组件CPU、内存、I/O讲起,这三者的处理速度差异非常大,CPU处理速度最快,内存次之,最后是IO设备。但是在绝大部分的程序中,一定会存在内存访问或则和IO设备访问,比如磁盘的访问。

为了提升计算性能,CPU从单核升级到多核,但是仅仅提升CPU的性能是不够的,因为如果后面两者的性能没跟上,计算的速度还是取决于最慢的设备。所以为了提升性能,从硬件上做了一些优化:

  • CPU增加了高速缓存
  • 操作系统增加了进程、线程等
  • 编译器的指令优化
CPU的高速缓存

假设我们现在是有两个CPU:CPU0跟CPU1,当CPU要去读取主内存的数据的时候,通过总线去读,所以为了提升速度,硬件在CPU与主内存中添加了CPU高速内存这种东西。

  • L1: 一级缓存,缓存在CPU中,是私有的,一个CPU有两块L1缓存,一个是指令缓存,一个是数据缓存
  • L2:二级缓存,也缓存在CPU中,私有,内存比L1稍微大一点。
  • L3:三级缓存,缓存在CPU与主内存之间,内存最大,共享的。

可以打开任务管理器查看

当我们的CPU0去读取主内存的数据i = 0的时候 ,会将数据缓存到CPU的缓存中,同样 CPU1去读取数据的时候也会缓存一份到缓存中,这样就很好的解决了处理器与内存的速度矛盾。

但是这个时候又出现了问题:当CPU0更改了i的值之后,会同步将i的值到主内存中,但是这个时候CPU1中也缓存了i的值 是0,CPU1还不知道主内存中的i的值已经被CPU0修改了,这个时候就会出现了缓存一致性的问题了。

为了解决上面的一致性问题。CPU就做出了两个解决方法,加锁

  • 总线锁
  • 缓存锁
总线锁与缓存锁

因为CPU在与内存拿数据的时候,一定是通过总线去拿的,所以就在总线加了锁,但是锁定了总线之后,其他的处理是无法通过总线去拿数据,又影响到了性能,这个时候就需要通过锁的控制力度来优化,所以又采用了缓存锁。而缓存锁是居于缓存一致性协议来实现的。

缓存一致性协议

为了达到数据访问的一致,各个处理器在访问缓存时就需要遵循一些协议,最常见的就是MESI协议

MESI表示缓存行的四种状态

  • M:Modify 表示共享数据只缓存在当前CPU缓存中,并且是被修改的状态,也就是此时缓存的数据与主内存中的数据是不一致的
  • E:Exclusive 表示缓存的独占状态,数据只缓存在当前的CPU缓存中,并且没有被修改
  • S: share 表示数据可能被多哥CPU缓存,并且各个CPU缓存中的数据和主内存一致
  • I:Invalod 表示缓存已经失效

对于MESI协议,从CPU读写角度来说会遵循以下原则:

CPU读请求:缓存处于M、E、S状态都可以被读取,I状态CPU只能从主内存中读取数据

CPU写请求:缓存处于M、E状态菜可以被写。对于S状态的写,需要将其他的CPU中缓存置为无效才可以写。所以使用了缓存锁机制之后,CPU对于内存的操作基本达到了缓存一致性的效果。

总结

由于CPU高速缓存的出现,使得多个CPU同时缓存了相同的共享数据时,可能存在可见性问题。也就是CPU0修改了自己缓存的值对于CPU1是不可见的,不可见导致了CPU1在后续对该数据进行写入的操作时,使用的是脏数据。使最终的结果错误。

另外还有一个问题就是线程的执行的顺序问题,因为多线程是无法控制哪个线程的某句代码会在另一个先册灰姑娘的某句代码后面执行,所以我们也就只能基于它的原理去了解一个这样存在的事实。

那么是不是有了缓存锁机制就能够达到缓存一致性的要求,那为什么还要加volatile关键字呢?

MESI带来的可见性问题

MESI协议虽然实现了缓存的一致性,但是同时又存在了一些问题:

各个CPU缓存的状态是通过消息传递来进行的。假设CPU0跟CPU1都缓存了 i = 0, 这个时候CPU0要对缓存中的共享变量i进行写入,首先就要发送一个失效的消息给CPU1,告诉CPU1它要开车了,然后还要等到CPU1收到消息之后再确认回执给回CPU0(有点像HTTP的三次握手)。CPU0这个时候都会处于阻塞状态。为了避免阻塞带来的资源浪费。在cpu中又引入了要给store bufferes。

CPU0只需要在写入共享数据时,直接把数据写入到store bufferes中,同时发送invalidate消息,然后继续去处理其他指令。当其他CPU发送了invalidate acknowledge消息时,再将store bufferes中的数据存储至cache line中。最后再从缓存同步到主内存中。

但是这里又会存在另外的问题。。。。

  • 数据什么时候提交根本不确定,因为要等待所有的其他cpu给回复才会进行数据同步。
  • 引入了store bufferes后,处理器会尝试优先从store bufferes中读取值,如果store bufferes中有值,则从store bufferes读取,否则再从缓存中读取。这个时候可能会存在CPU的乱序执行,也可以认为是一种重排序(不详细介绍),而重排序也会带来可见性的问题。

这个时候,,,反正怎么优化都不符合要求,硬件层面就把执行权给到软件了,所以CPU层面提供了内存屏障指令,在软件层面可以决定在适当的地方来使用内存屏障。

那内存屏障如何来加?其实就是volatile关键字,前面说到,volatile会在汇编指令中加入一个lock的指令,这个指令其实相当于实现了一种内存屏障。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java患者 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 初步认识
  • volatile的作用
  • volatile的原理是什么
  • 可见性的本质
    • CPU的高速缓存
      • 总线锁与缓存锁
        • 缓存一致性协议
        • 总结
        • MESI带来的可见性问题
        相关产品与服务
        数据保险箱
        数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档