专栏首页Java患者你连volatile都不在意,你在意什么,在意大利吗

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

深入理解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的指令,这个指令其实相当于实现了一种内存屏障。

本文分享自微信公众号 - Java患者(gh_3a16ffdedb6a),作者:Zero

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-07-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 你永远不知道你在用户协议里同意了什么

    互联网赋予了你足不出户的生活资本,而你需要拿出来置换的,是你的个人信息,你的位置,你的肖像权。

    释然
  • MIT在读博士心得:做好AI科研,你需要注意什么?

    用户1737318
  • 【第四期】你在 GitHub 上看到过的最有意思的项目是什么?

    GitHub地址:https://github.com/wb14123/seq2seq-couplet

    良月柒
  • 围绕一个 volatile 关键字居然可以问出来 16 个问题

    对于 Java 每次面试就会想到多线程,多线程问题基本跑不了要问一下 volalite 关键字,可是我万万没想到居然一个 volatile 关键字可以连续问题出...

    Java团长
  • 【DL笔记8】如果你愿意一层一层剥开CNN的心——你会明白它究竟在做什么

    从【DL笔记1】到【DL笔记N】,是我学习深度学习一路上的点点滴滴的记录,是从Coursera网课、各大博客、论文的学习以及自己的实践中总结而来。从基本的概念、...

    beyondGuo
  • 央视请你在VR中看《昆曲涅槃》,对文化遗产来说VR意味着什么?

    走进沧浪亭的那一刻,恍惚间,尘世仿佛退回百年。“清风明月本无价,近水遥山皆有情。”一副刻在园内的对联,上联出自欧阳修,下联出自苏舜钦,也只有这样的沧浪亭,才当得...

    VRPinea
  • 2019年Java面试题基础系列228道(4),快看看哪些你还不会?

    https://cloud.tencent.com/developer/article/1549815

    程序员追风
  • 【Java后端面试经历】我和阿里面试官的“又”一次“邂逅”(附问题详解)

    承接上一篇深受好评的文章:《【Java 大厂真实面试经历】我和阿里面试官的一次“邂逅”(附问题详解)》 。时隔 n 个月,又一篇根据读者投稿的《5 面阿里,终获...

    Guide哥
  • 记一次大厂面试,成功拿到offer!

    若大家看到这类干货文或者觉得很不错的技术文,可后台或者留言区留言,场主会优选,将好文分享给更多的技术人!

    养码场
  • Redis过期策略以及淘汰机制

    Redis中可以通过expire设置键的过期,那么Redis又是什么时候删除键的呢?

    用户7386338
  • 美团大零售事业群-闪购 一面(已通过)

    以前以为坚持就是永不动摇,现在才明白,坚持是犹豫着退缩着心猿意马着,但还在继续往前走。——《意林》

    牛客网
  • Java 面试问题大全

    能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。我的意思是,如果改变引用指向的数组,将会受到 volatile...

    时代疯
  • 分享 Java 常见面试题及答案(上)

    能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。我的意思是,如果改变引用指向的数组,将会受到 volatile...

    程序IT圈
  • 可见性有序性,Happens-before来搞定

    上一篇文章并发 Bug 之源有三,请睁大眼睛看清它们 谈到了可见性/原子性/有序性三个问题,这些问题通常违背我们的直觉和思考模式,也就导致了很多并发 Bug

    用户4172423
  • 2 万多字,183 道 Java 面试题分析及答案

    除了你看到的惊人的问题数量,我也尽量保证质量。我不止一次分享各个重要主题中的问题,也确保包含所谓的高级话题,这些话题很多程序员不喜欢准备或者直接放弃,因为他们的...

    业余草
  • 面试官问我单例模式真的安全吗?我懵逼了

    某天晚上,我和基友正在开黑排位,刚刚被敌方EZ用E躲了我混分巨兽的石破天惊,恼羞成怒的我正准备咬牙切齿还回去时,手机响了。

    大王叫下
  • 疯转|最近5年133个Java面试问题列表

    Java 面试随着时间的改变而改变。在过去的日子里,当你知道 String 和 StringBuilder 的区别就能让你直接进入第二轮面试,但是现在问题变得越...

    用户1257215
  • 关于线程,还有这些是你需要知道的!

    在日常开发中,线程常常被用作为提升程序效率的重要手段。在CoorChice的这篇文章中,CoorChice介绍了线程的基本运作。【你知道Thread线程是如何运...

    陈宇明
  • java学习要点

    作为一个程序员,在找工作的过程中,都会遇到笔试,而很多笔试里面都包括java,尤其是作为一个Android开发工程师,java是必备技能之一.所以为了笔试过程中...

    仇诺伊

扫码关注云+社区

领取腾讯云代金券