JMM内存交互层面实现
volatile修饰的变量的read、load、use操作和assign、store、write必须是连续(未使用volatile修饰,仅保证顺序执行,不要求连续)的,即修改后必须立即同步回主内存,使用时必须从主内存刷新,由此保证volatile变量操作对多线程的可见性。
硬件层面实现
通过lock前缀指令,会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

volatile在hotspot的实现

字节码解释器实现

JVM中的字节码解释器(bytecodeInterpreter),用C++实现了JVM指令,其优点是实现相对简单且容易理解,缺点是执行慢。
bytecodeInterpreter.cpp
image.png

模板解释器实现

模板解释器(templateInterpreter),其对每个指令都写了一段对应的汇编代码,启动时将每个指令与对应汇编代码入口绑定,可以说是效率做到了极致。
templateTable_x86_64.cpp

  1. void TemplateTable::volatile_barrier(Assembler::Membar_mask_bits
  2. order_constraint) {
  3. // Helper function to insert a is-volatile test and memory barrier
  4. if (os::is_MP()) { // Not needed on single CPU
  5. __ membar(order_constraint);
  6. }
  7. }
  8. // 负责执行putfield或putstatic指令
  9. void TemplateTable::putfield_or_static(int byte_no, bool is_static, RewriteControl rc) {
  10. // ...
  11. // Check for volatile store
  12. __ testl(rdx, rdx);
  13. __ jcc(Assembler::zero, notVolatile);
  14. putfield_or_static_helper(byte_no, is_static, rc, obj, off, flags);
  15. volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad |
  16. Assembler::StoreStore));
  17. __ jmp(Done);
  18. __ bind(notVolatile);
  19. putfield_or_static_helper(byte_no, is_static, rc, obj, off, flags);
  20. __ bind(Done);
  21. }

assembler_x86.hpp

  1. // Serializes memory and blows flags
  2. void membar(Membar_mask_bits order_constraint) {
  3. // We only have to handle StoreLoad
  4. // x86平台只需要处理StoreLoad
  5. if (order_constraint & StoreLoad) {
  6. int offset = -VM_Version::L1_line_size();
  7. if (offset < -128) {
  8. offset = -128;
  9. }
  10. // 下面这两句插入了一条lock前缀指令: lock addl $0, $0(%rsp)
  11. lock(); // lock前缀指令
  12. addl(Address(rsp, offset), 0); // addl $0, $0(%rsp)
  13. }
  14. }

在linux系统x86中的实现

orderAccess_linux_x86.inline.hpp

  1. inline void OrderAccess::storeload() { fence(); }
  2. inline void OrderAccess::fence() {
  3. if (os::is_MP()) {
  4. // always use locked addl since mfence is sometimes expensive
  5. #ifdef AMD64
  6. __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
  7. #else
  8. __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
  9. #endif
  10. }

x86处理器中利用lock实现类似内存屏障的效果

lock前缀指令的作用

  1. 确保后续指令执行的原子性。在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销。
  2. LOCK前缀指令具有类似于内存屏障的功能,禁止该指令与前面和后面的读写指令重排序。
  3. LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新store buffer的操作会导致其他cache中的副本失效

    汇编层面volatile的实现

    添加下面的jvm参数查看之前可见性Demo的汇编指令
    -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
    image.png

    从硬件层面分析Lock前缀指令

    32位的IA-32处理器支持对系统内存中的位置进行锁定的原子操作。这些操作通常用于管理共享的数据结构(如信号量、段描述符、系统段或页表),在这些结构中,两个或多个处理器可能同时试图修改相同的字段或标志。处理器使用三种相互依赖的机制来执行锁定的原子操作:

    • 有保证的原子操作
    • 总线锁定,使用LOCK#信号和LOCK指令前缀
    • 缓存一致性协议,确保原子操作可以在缓存的数据结构上执行(缓存锁);这种机制出现在Pentium 4、Intel Xeon和P6系列处理器中