深入浅出计算机组成原理
徐文浩
bothub 创始人
70432 人已学习
新⼈⾸单¥68
登录后,你可以任选4讲全文学习
课程目录
已完结/共 62 讲
深入浅出计算机组成原理
15
15
1.0x
00:00/00:00
登录|注册

38 | 高速缓存(下):你确定你的数据更新了么?

MESI协议
写回(Write-Back)
写直达(Write-Through)
性能差异的实验
相关文章推荐
解决缓存一致性问题的挑战
Java内存模型和CPU、CPU Cache以及主内存的相似性
缓存一致性问题
CPU Cache的写入策略
Cache和主内存的数据同步问题
示例程序演示volatile关键字的作用
与锁或原子性操作无关
保障对数据的读写都会同步到主内存
JMM和计算机组成里的CPU、高速缓存和主内存组合在一起的硬件体系相似
课后思考
推荐阅读
总结延伸
CPU高速缓存的写入
隐身的变量
volatile关键字
Java内存模型(JMM,Java Memory Model)
文章:我们写入的数据,到底应该写到Cache里还是主内存里?

该思维导图由 AI 生成,仅供参考

在我工作的十几年里,写了很多 Java 的程序。同时,我也面试过大量的 Java 工程师。对于一些表示自己深入了解和擅长多线程的同学,我经常会问这样一个面试题:“volatile 这个关键字有什么作用?”如果你或者你的朋友写过 Java 程序,不妨来一起试着回答一下这个问题。
就我面试过的工程师而言,即使是工作了多年的 Java 工程师,也很少有人能准确说出 volatile 这个关键字的含义。这里面最常见的理解错误有两个,一个是把 volatile 当成一种锁机制,认为给变量加上了 volatile,就好像是给函数加了 sychronized 关键字一样,不同的线程对于特定变量的访问会去加锁;另一个是把 volatile 当成一种原子化的操作机制,认为加了 volatile 之后,对于一个变量的自增的操作就会变成原子性的了。
// 一种错误的理解,是把volatile关键词,当成是一个锁,可以把long/double这样的数的操作自动加锁
private volatile long synchronizedValue = 0;
// 另一种错误的理解,是把volatile关键词,当成可以让整数自增的操作也变成原子性的
private volatile int atomicInt = 0;
amoticInt++;
事实上,这两种理解都是完全错误的。很多工程师容易把 volatile 关键字,当成和锁或者数据数据原子性相关的知识点。而实际上,volatile 关键字的最核心知识点,要关系到 Java 内存模型(JMM,Java Memory Model)上。
虽然 JMM 只是 Java 虚拟机这个进程级虚拟机里的一个内存模型,但是这个内存模型,和计算机组成里的 CPU、高速缓存和主内存组合在一起的硬件体系非常相似。理解了 JMM,可以让你很容易理解计算机组成里 CPU、高速缓存和主内存之间的关系。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

Java中的volatile关键字在多线程编程中起着重要作用。本文通过一个经典的volatile代码示例,深入探讨了volatile关键字的作用和原理。作者通过对比有无volatile关键字的情况下,对COUNTER变量的读取和写入行为,解释了volatile关键字确保数据读写同步到主内存而不是从缓存中读取的机制。通过实验和分析,阐述了volatile关键字在多线程环境下的重要性,以及与Java内存模型的关系。文章通过生动的例子和清晰的解释,帮助读者深入理解volatile关键字的作用和原理,为多线程编程提供了重要的参考和指导。 文章还介绍了CPU高速缓存的写入策略,包括写直达和写回两种策略,以及缓存一致性的问题。通过对比这些策略的性能和效果,读者可以更好地理解Java内存模型和CPU、CPU Cache以及主内存的组织结构。此外,文章提到了MESI协议作为维护缓存一致性的方法,为读者提供了进一步的思考和学习方向。 总的来说,本文内容丰富,涵盖了volatile关键字、CPU高速缓存的写入策略以及缓存一致性等多个重要主题,对于想深入了解多线程编程和底层硬件架构的读者来说,具有很高的参考价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《深入浅出计算机组成原理》
新⼈⾸单¥68
立即购买
登录 后留言

全部留言(51)

  • 最新
  • 精选
  • Knight²º¹⁸
    Java sleep 解释有问题,sleep 并不是说线程有时间去主内存中读取变量,而是 sleep 的线程会让出cpu,线程被唤醒后才会去重新加载变量。

    作者回复: Knight²º¹⁸同学, 你好,你说的有道理,我去修改一下。

    2019-12-20
    7
    89
  • 程序员花卷
    不加volatitle关键字 private static int num = 1; public static void main(String[] args) { int[] arr = new int[8000000]; for (int i = 0; i < 8000000; i++) { arr[i] = num; num++; } 运行时间为:28毫秒 加了关键字 private static volatile int num = 1; public static void main(String[] args) { int[] arr = new int[8000000]; for (int i = 0; i < 8000000; i++) { arr[i] = num; num++; } 运行的时间为128毫秒

    作者回复: Hash同学, 👍,加油,从实践中有所体会是最好的学习方法。

    2019-12-23
    2
    22
  • 许先森
    文中有写:“写回策略的过程是这样的:如果发现我们要写入的数据,就在 CPU Cache 里面,那么我们就只是更新 CPU Cache 里面的数据。同时,我们会标记 CPU Cache 里的这个 Block 是脏(Dirty)的。所谓脏的,就是指这个时候,我们的 CPU Cache 里面的这个 Block 的数据,和主内存是不一致的。” 所以图里的Cache Block指的是CPU高速缓存块,不是内存块。

    作者回复: 许先森同学, 是的,这里指的是Cache,不是主内存。

    2020-01-17
    2
  • 花晨少年
    讲得好啊,透彻

    作者回复: 🙏 谢谢支持

    2019-12-15
    2
  • LDxy
    volatile关键字在用C语言编写嵌入式软件里面用得很多,不使用volatile关键字的代码比使用volatile关键字的代码效率要高一些,但就无法保证数据的一致性。volatile的本意是告诉编译器,此变量的值是易变的,每次读写该变量的值时务必从该变量的内存地址中读取或写入,不能为了效率使用对一个“临时”变量的读写来代替对该变量的直接读写。编译器看到了volatile关键字,就一定会生成内存访问指令,每次读写该变量就一定会执行内存访问指令直接读写该变量。若是没有volatile关键字,编译器为了效率,只会在循环开始前使用读内存指令将该变量读到寄存器中,之后在循环内都是用寄存器访问指令来操作这个“临时”变量,在循环结束后再使用内存写指令将这个寄存器中的“临时”变量写回内存。在这个过程中,如果内存中的这个变量被别的因素(其他线程、中断函数、信号处理函数、DMA控制器、其他硬件设备)所改变了,就产生数据不一致的问题。另外,寄存器访问指令的速度要比内存访问指令的速度快,这里说的内存也包括缓存,也就是说内存访问指令实际上也有可能访问的是缓存里的数据,但即便如此,还是不如访问寄存器快的。缓存对于编译器也是透明的,编译器使用内存读写指令时只会认为是在读写内存,内存和缓存间的数据同步由CPU保证。
    2019-07-22
    6
    145
  • 林三杠
    反复看了几次写回策略,才看明白。主要是“如果我们发现,我们要写入的数据所对应的 Cache Block 里,放的是别的内存地址的数据”这句。同一个cache地址可能被多个进程使用,使用前需要确认是否是自己的数据,是的话,直接写,不是自己的而且被标记为脏数据,需要同步回主内存。老师,我理解的对吧?
    2019-07-22
    8
    20
  • 西门吹牛
    volatile 关键字去掉,变量的更新是先从内存中把变量加载到自己的缓存, ChangeMaker 线程把变量COUNTER = 0 加载到自己的缓存,并在自己的缓存内更新。ChangeMaker 每次更新完成后,会进行sleep,此时回把更新的数据同步到内存中。而 ChangeListener 线程,也是先从内存中获取数据,因为他自己的缓存内没有该变量。ChangeListener 线程,第一次从内存中读取到的变量值是0,因为车此时 ChangeMaker 线程对变量的更新还没有同步到内存,ChangeListener 线程从内存读取到0,并把该值加载到缓存,之后进行循环,每次循环都是从自己的缓存中读取数据,所以ChangeListener线程从每次循环从缓存中获取的变量值是0; ChangeListener 线程修改为在循环内Thread.sleep(5)。线程休眠结束后,每次休眠结束,线程都会在从内存中在读取一次数据,这时休眠时间为5秒,刚好ChangeMaker 线程每次更新也休眠5秒,这时,ChangeMaker线程对变量的更新,在同步到内存后,刚好被ChangeListener 线程读取到; 所以,能得出结论,sleep之后,线程有足够的时间将缓存同步到内存,如果没有sleep,线程一直在执行,就没有时间将缓存数据同步到内存,同时,每次sleep之后,线程都会从内存中在读取一次数据到缓存,而不是sleep之后,还是读取自己的缓存数据。 Java 内存模型是一个隔离了硬件实现的虚拟机内的抽象模型,不同的线程或 CPU 核有着自己各自的缓存,缓存会导致可见性问题,可见性是并发bug的源头之一。所以java引入volatile关键字,能解决缓存带来的线程之间可见性的问题。java内存模型中规定,一个线程对volatile修饰变量的写操作先发与另一线程对于该变量的读操作,也就是说,针对volatile修饰的变量,一个线程要想读取到别的线程更新后的数据,就必须从内存中读取,而一个线程的写操作要想被别的线程看到,就必须保证在更新完之后,同步到内存中。所以volatile关键字的作用,就是确保变量的读取和写入,一定会同步到主内存,而不基于cpu缓存中的数据进行读取和写入。 要实现对volatile修饰的变量,每次的读取和写入,一定会同步到主内存。java的实现方案是利用内存屏障来实现,而内存屏障的实现,是基于cpu指令来实现的。经过volatile修饰的变量。在经过jvm解释器解释成机器码后,都会插入一写内存屏障的cpu指令,这些cpu指令的作用就是确保,每次对volatile修饰的变量的更新,都必须同步到内存,而每次读取volatile修饰的变量,都必须从内存中获取,而不是直接从cpu缓存获取。 所以Java内存模型隔离了具体的硬件实现,这些内存屏障的指令都是jvm在解释执行的时候加上的,程序员只需要在代码中用volatile 修饰即可,至于volatile 底层的实现,都是基于 java 的内存模式实现的。
    2020-07-08
    1
    11
  • Monday
    简而言之,volatile变量就是禁用Cache
    2020-06-13
    1
    9
  • 树军
    老师,这讲里对volatile的解释是完全错的,cache从CPU的角度来看,对程序员是透明的,从软件看过去不会存在不一致的情况,只有在多master访问的时候才会关心,比如DMA等。这里的不一致不是由cache造成的,而是编译器对变量优化造成的,忙等待中,如果没有volatile关键字,编译器认为这个变量不会被改变,分配一个临时变量,一般就是一个寄存器,每次访问都直接访问寄存器,而不去访问真实的地址造成的
    2020-08-06
    3
    6
  • 曙光
    看了后面MESI协议的介绍,反而对本章示例程序有疑问,程序(2)中,虽然去掉了volatile的关键字,但ChangeListener应该接收到“写失效”的广播,然后中断忙等,再去内存获取最新数据。那有没有广播到ChangeListener的cpu cache呢? 本人i5-8250U, ChangeListener需要至少大于等于Thread.sleep(495)才能和程序(1)的测试结果一样,这是咋回事?
    2019-10-22
    5
收起评论
显示
设置
留言
51
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部