作者回复: volatile字段的happens-before关系指的是在两个不同线程中,volatile的写操作 happens-before 之后对同一字段的读操作。这里有个关键字之后,指的是时间上的先后。也就是我这边写,你之后再读就一定能读得到我刚刚写的值。普通字段则没有这个保证。
屏障不允许重排序是针对即时编译器的。写后对同一字段的读,属于数据依赖,本来也不可以重排序的。
作者回复: 我可能没有在原文中讲清楚。这里指的是volatile变量不能被分配到寄存器中,但是计算还是加载到寄存器中来计算的。
所谓的分配到寄存器中,你可以理解为编译器将内存中的值缓存在寄存器中,之后一直用访问寄存器来代表对这个内存的访问的。假设我们要遍历一个数组,数组的长度是内存中的值。由于我们每次循环都要比较一次,因此编译器决定把它放在寄存器中,免得每次比较都要读一次内存。对于会更改的内存值,编译器也可以先缓存至寄存器,最后更新回内存即可。
Volatile会禁止上述优化。
作者回复: 即时编译器生成的代码里会使用CPU的内存屏障指令。HotSpot采用的lock前缀的指令,lock add DWORD PTR [rsp] 0。它也会刷缓存。
至于在即时编译器里禁止重排序所使用的”内存屏障”,就是一个特殊的编译器中间表达形式节点。
作者回复: 工作内存是JMM抽象出来的一个概念。你可以映射到实际的CPU缓存。
作者回复: 文末[4]所指向的链接。
作者回复: 对的!如果即时编译器把那个变量放在寄存器里维护,那么另一个线程也没辙。
作者回复: 首先,b加了volatile之后,并不能保证b=1一定先于r1=b,而是保证r1=b始终能够看到b的最新值。比如说b=1;b=2,之后在另一个CPU上执行r1=b,那么r1会被赋值为2。如果先执行r1=b,然后在另外一个CPU上执行b=1和b=2,那么r1将看到b=1之前的值。
在没有标记volatile的时候,同一线程中,r2=a和b=1存在happens before关系,但因为没有数据依赖可以重排列。一旦标记了volatile,即时编译器和CPU需要考虑到多线程happens-before关系,因此不能自由地重排序。
作者回复: 谢谢总结!确实,本文重点讲的是内存可见性规则。
JMM的工作内存,主内存这些概念都是抽象的,对应实际体系架构中的缓存和内存。本文切掉了抽象的那部分,直接用实际的体系架构来讲解。
作者回复: 嗯,我回到原问题上哈
作者回复: 理论上,因为都使用同一套缓存,所以不需要volatile。实际实现中,对编译器不能重排列的限制还是存在的,但具体的memory barrier指令的实现是no-op。
作者回复: 在解释执行时,字节码之间也有内存屏障
作者回复: 我们考虑一种简单情况,即每个CPU有独占缓存,没有共享缓存。强制刷新缓存,是为了让跑在另外一个CPU上的线程看到你这个CPU上更新的内容。
如果想要深入研究下去,可以翻翻CSAPP那本书。