vlambda博客
学习文章列表

从Hotspot源码分析volatile的实现原理

   本人开源了重试利器 Attempt 感兴趣的可以去 clone下代码阅读,也欢迎 start 和 discuss。gitHub 搜索:IceFrozen/Attempt


从Hotspot源码分析volatile的实现原理

从Hotspot源码分析volatile的实现原理


0

概诉

从Hotspot源码分析volatile的实现原理


volatile关键字是Java并发编程当中不可或缺的一环,维护着Java共享变量的可见性和有序性。由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来回顾一下volatile的应用场景。

Java是跨平台的,因此针对于不同平台,JVM实现方式也有所不同,但是必遵循一定的规范,来统一Java编程的基本原则,这个规范被称为Java虚拟机规范。
Java内存模型( Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。具体内容请参考我的另一篇博文[ Java内存模型(1)JMM是什么]( https://icefrozen.github.io/article/java-memory-model/)。


从Hotspot源码分析volatile的实现原理


1

volatile 应用场景

从Hotspot源码分析volatile的实现原理

01

 可见性



首先我们来看一段代码:

从Hotspot源码分析volatile的实现原理


上面这段代码可能永远也不会结束,因为线程t1对 isStop的修改,线程t2可能对此并不可见。当然只是可能,所以为了放大可见性问题,我这里进行了25次循环。只要有一组线程发生“线程t1对 isStop的赋值,线程t2对此不可见”的情况,就不会退出程序。

假如你给 isStop添加一个 volatile 关键字,那么你会发现程序立马就会退出。 针对于可见性,还有一个经典的double check例子:

从Hotspot源码分析volatile的实现原理



02

 有序性



下面我们来看有序性。

从Hotspot源码分析volatile的实现原理


运行结果如下:

执行次数:247521,x:0 y:0
上述代码中,循环启动线程A,B,如果说x,y都等于0时,程序退出。count是程序次数计数器。下图是控制台程序打印部分结果。从图上可以分析出x,y都等于0时,线程A的a = 3; x = b;两行代码做了重排序,线程B中 b = 3;y = a;两行代码也做了重排序。
这就是JIT编译器优化代码重排序后的结果。 volatile 关键字是具有屏障功能的,也就是说 volatile变量的写前后,编译器不可以进行重排。


看第二个例子:

从Hotspot源码分析volatile的实现原理


如上述代码,将屏障放在中间,会禁止上下指令重排,x,y变量不可能同时为0,该程序会一直陷入死循环,结束不了。


从Hotspot源码分析volatile的实现原理

2

原理

从Hotspot源码分析volatile的实现原理

总的来说,volatile可以保证在并发环境下共享变量的可见性和有序性。那么,之所以产生有序性和可见性的原因有两点:

     1、 指令重排序

     2、 MESI协议的导致的缓存未刷新。

01

 指令重排序




而指令重排序,则又分为, 编译器重排序和 CPU重排序,这里重排序的原因不再赘述,详情见[ Java内存模型(2)编译器重排序]( https://icefrozen.github.io/article/java-memory-model-complier-reordering/)和[ Java内存模型(1)JMM是什么]( https://icefrozen.github.io/article/java-memory-model/),这里面详细阐述了CPU指令重排序和编译器重排序的原理。
剩下的就是MESI协议导致的不可见原因,详情可以参考[ CPU缓存一致性协议-深入理解内存屏障]( https://icefrozen.github.io/article/why-memory-barriers-1/), MESI协议是一个基础协议,在不同架构下的CPU会有不同形式的实现,例如X86下的CPU使用采用的是改进型的MESI协议。

因此,了解产生原因之后,我们就需要提出解决办法,思路就是很清楚了 1、禁止指令重排序,2、强制令MESI协议中的中间状态刷入缓存当中

如果你了解MESI协议,那么事实上,MESI协议其实也是一种指令重排,只不过是伪重排,这和CPU重排序是一个道理,CPU并没有将指令的顺序发生改变,而是各个指令的执行完的时间不是一样的,也就是说,顺序执行,乱序完成。

02

 内存屏障




了解这个事实,解决思路就非常明了,那就是 内存屏障。由于不同的平台CPU实现的机制不同,各个编译器对代码的优化方式不同,因此,JMM不得不考虑一种规则,适用于所有的平台,因此,JMM规定了四种屏障:

从Hotspot源码分析volatile的实现原理

  1. 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。


  2. 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。


  3. 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。


为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。 对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。 为此,JMM采取保守策略。 下面是基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。

  • 在每个volatile写操作的后面插入一个StoreLoad屏障。

  • 在每个volatile读操作的后面插入一个LoadLoad屏障。

  • 在每个volatile读操作的后面插入一个LoadStore屏障。


从Hotspot源码分析volatile的实现原理


上图的 StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见了,因为 StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。 其他屏障也有 类似的效果:

从Hotspot源码分析volatile的实现原理

从Hotspot源码分析volatile的实现原理


总结来说,volatile 可见性包括两个方面:

  • 写入的 volatile 变量在写完之后能被别的 CPU 在下一次读取中读取到;

  • 写入 volatile 变量之前的操作在别的 CPU 看到 volatile 的最新值后一定也能被看到;

对于第一个方面,主要通过:

  • 读取 volatile 变量不能使用寄存器,每次读取都要去内存拿;

  • 禁止读 volatile 变量后续操作被重排到读 volatile 之前;


对于第二个方面,主要是通过写 volatile 变量时的 Barrier 保证写 volatile 之前的操作先于写 volatile 变量之前发生。最后还一个特殊的,如果能用到 StoreLoad Barrier,写 volatile 后一般会触发 Store Buffer 的刷写,所以写操作能「 立即」被别的 CPU 看到。

03

 x86平台下的内存屏障



在[ CPU缓存一致性协议(2)- 缓存一致性性模型 ]( https://icefrozen.github.io/article/why-memory-barriers-2/  我们已经讨论了x86下TSO模型是只有 StoreLoad 是允许重排序的,也就是说 MESI协议的StoreBuffer的引入,使得StoreLoad 有乱序执行的可能,因此,我们需要 禁止 StoreLoad的排序。 而其他的屏障,我们只需要保证,编译器不会在编译期间,进行优化就可以了。


2

HotSpot屏障的实现

从Hotspot源码分析volatile的实现原理


接下来,我们将会以JDK1.8的源码来分析一下 volatile 的实现原理。

01

 字节码上的区别



从Hotspot源码分析volatile的实现原理


上述代码编译后,我们用javap命令来看一下字节码:

//  volatile int val; descriptor: I flags: ACC_VOLATILE
int val2; descriptor: I flags:
// public int add(); descriptor: ()I flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #2 // Field val:I 5: iconst_1 6: iadd 7: putfield #2 // Field val:I 10: aload_0 11: getfield #2 // Field val:I 14: ireturn LineNumberTable: line 7: 0 line 8: 10
public int add2(); descriptor: ()I flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #3 // Field val2:I 5: iconst_1 6: iadd 7: putfield #3 // Field val2:I 10: aload_0 11: getfield #3 // Field val2:I 14: ireturn LineNumberTable: line 11: 0 line 12: 10}


从javap的输出来看, volatile和非 volatile变量没有任何区别,从Java虚拟机规范可知,某个字段是否是volatile变量是通过用来描述字段属性的access_flags来决定的,通过特定的标识位来识别,如下图:

从Hotspot源码分析volatile的实现原理


从javap的输出分析可知,并没有专门针对volatile变量的特殊字节码指令,其处理逻辑还是在属性读写的字节码指令中,相关的指令有四个: _getstatic/_putstatic,_getfield/_putfield

JVM执行字节码需要解释器( Interpreter)。HotSpot虚拟机中支持两种解释器—— CppInterpreter 和 TemplateInterpreter,对于一个编译好的JVM可执行程序来说,其使用的解释器是固定的,不可根据运行时条件改变,绝大多数JVM实例都使用 TemplateInterpreter,下面也将分析基于 TemplateInterpreter 的字节码执行。


以x86平台为例,对于 TemplateInterpreter,字节码对应的C/C++/汇编实现存放在 /src/hotspot/cpu/x86/templateTable_x86.cpp文件中。 X86 平台下 getfield putfield 也字节码的实现在其中。


02

 读操作


void TemplateTable::getfield(int byte_no) { getfield_or_static(byte_no, false);}
_getstatic / _getfield适用于所有类型的字段属性读取,因此在具体实现时需要根据flags中保存的属性类型适配对应的处理逻辑,为了避免每次都要判断属性类型,OpenJDK增加了几个自定义的带目标类型的属性读取的字节码指令,如 _fast_igetfield
void TemplateTable::getfield_or_static(int byte_no, bool is_static, RewriteControl rc) { transition(vtos, vtos); const Register cache = rcx; const Register index = rdx; const Register obj = c_rarg3; const Register off = rbx; const Register flags = rax; const Register bc = c_rarg3; // uses same reg as obj, so don't mix them  //给该字段创建一个ConstantPoolCacheEntry,该类表示常量池中某个方法或者字段的解析结果 resolve_cache_and_index(byte_no, cache, index, sizeof(u2)); //发布jvmti事件 jvmti_post_field_access(cache, index, is_static, false); //加载该字段的偏移量,flags,如果是静态字段还需要解析该类class实例对应的oop load_field_cp_cache_entry(obj, cache, index, off, flags, is_static);  if (!is_static) { //将被读取属性的oop放入obj中 pop_and_check_object(obj); }  const Address field(obj, off, Address::times_1);  Label Done, notByte, notBool, notInt, notShort, notChar, notLong, notFloat, notObj, notDouble;  __ shrl(flags, ConstantPoolCacheEntry::tos_state_shift); // Make sure we don't need to mask edx after the above shift assert(btos == 0, "change code, btos != 0");  __ andl(flags, ConstantPoolCacheEntry::tos_state_mask); //判断是否是byte类型 __ jcc(Assembler::notZero, notByte); //读取该属性,并放入rax中 __ load_signed_byte(rax, field); __ push(btos); if (!is_static) { //将该指令改写成_fast_bgetfield,下一次执行时就是_fast_bgetfield patch_bytecode(Bytecodes::_fast_bgetfield, bc, rbx); } //跳转到Done __ jmp(Done);  __ bind(notByte); //判断是否boolean类型 __ cmpl(flags, ztos); __ jcc(Assembler::notEqual, notBool);  // ztos (same code as btos) __ load_signed_byte(rax, field); __ push(ztos); // Rewrite bytecode to be faster if (!is_static) { // use btos rewriting, no truncating to t/f bit is needed for getfield. patch_bytecode(Bytecodes::_fast_bgetfield, bc, rbx); } __ jmp(Done);  __ bind(notBool); //判断是否引用类型 __ cmpl(flags, atos); __ jcc(Assembler::notEqual, notObj); // atos __ load_heap_oop(rax, field); __ push(atos); if (!is_static) { patch_bytecode(Bytecodes::_fast_agetfield, bc, rbx); } __ jmp(Done);  __ bind(notObj); //判断是否int类型 __ cmpl(flags, itos); __ jcc(Assembler::notEqual, notInt); // itos __ movl(rax, field); __ push(itos); // Rewrite bytecode to be faster if (!is_static) { patch_bytecode(Bytecodes::_fast_igetfield, bc, rbx); } __ jmp(Done);  __ bind(notInt); //判断是否char类型 __ cmpl(flags, ctos); __ jcc(Assembler::notEqual, notChar); // ctos __ load_unsigned_short(rax, field); __ push(ctos); // Rewrite bytecode to be faster if (!is_static) { patch_bytecode(Bytecodes::_fast_cgetfield, bc, rbx); } __ jmp(Done);  __ bind(notChar); //判断是否short类型 __ cmpl(flags, stos); __ jcc(Assembler::notEqual, notShort); // stos __ load_signed_short(rax, field); __ push(stos); // Rewrite bytecode to be faster if (!is_static) { patch_bytecode(Bytecodes::_fast_sgetfield, bc, rbx); } __ jmp(Done);  __ bind(notShort); //判断是否long类型 __ cmpl(flags, ltos); __ jcc(Assembler::notEqual, notLong); // ltos __ movq(rax, field); __ push(ltos); // Rewrite bytecode to be faster if (!is_static) { patch_bytecode(Bytecodes::_fast_lgetfield, bc, rbx); } __ jmp(Done);  __ bind(notLong); //判断是否float类型 __ cmpl(flags, ftos); __ jcc(Assembler::notEqual, notFloat); // ftos __ movflt(xmm0, field); __ push(ftos); // Rewrite bytecode to be faster if (!is_static) { patch_bytecode(Bytecodes::_fast_fgetfield, bc, rbx); } __ jmp(Done);  __ bind(notFloat); // 只剩一种double类型 __ movdbl(xmm0, field); __ push(dtos); // Rewrite bytecode to be faster if (!is_static) { patch_bytecode(Bytecodes::_fast_dgetfield, bc, rbx); }  __ bind(Done); // [jk] not needed currently // volatile_barrier(Assembler::Membar_mask_bits(Assembler::LoadLoad | // Assembler::LoadStore));

这里有几个概念解释一下:


_fast_bgetfield 由于JVM的模板解释器的原理很简单,将java字节码所要执行的操作以汇编代码方式(这里说汇编是针对于不同的cpu写出不同的汇编代码)然后在内存中申请一块区域,将代码(也就是二进制流)放到这块区域当中,搞一个函数指针指向这块区域。
当然,在执行的时候会直接执行这块区域,那么这就需要操作系统允许应用程序开辟内存,且可以执行这块内存的代码。所以,汇编指令在编写的时候,很关心需要操作的栈顶元素以及要操作的数据的长度。
因此,有一系列的 _fast_bgetfield 汇编代码模板。而 getstatic / _getfield适用于所有类型的字段属性读取,因此在具体实现时需要根据flags中保存的属性类型适配对应的处理逻辑,为了避免每次都要判断属性类型。定义在 `hotspot/src/share/vm/interpreter/templateTable.cpp`里。

从Hotspot源码分析volatile的实现原理


__ __ 就是定义一个宏,泛指字节码解释器。 `#define __ masm->`masm 指的就是 MacroAssembler`这个类(当然,我是粗略的看了一下,并没有验证,这点存疑) `MacroAssembler` 就是具体将模板程和JVM的一个媒介。具体如何工作,这里就不展开。直接放实现 fast_accessfield 的实现


void TemplateTable::fast_accessfield(TosState state) { transition(atos, state);  //发布JVMTI事件 if (JvmtiExport::can_post_field_access()) { // Check to see if a field access watch has been set before we // take the time to call into the VM. Label L1; __ mov32(rcx, ExternalAddress((address) JvmtiExport::get_field_access_count_addr())); __ testl(rcx, rcx); __ jcc(Assembler::zero, L1); // access constant pool cache entry __ get_cache_entry_pointer_at_bcp(c_rarg2, rcx, 1); __ verify_oop(rax); __ push_ptr(rax); // save object pointer before call_VM() clobbers it __ mov(c_rarg1, rax); // c_rarg1: object pointer copied above // c_rarg2: cache entry pointer __ call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::post_field_access), c_rarg1, c_rarg2); __ pop_ptr(rax); // restore object pointer __ bind(L1); }  //获取该字段对应的ConstantPoolCacheEntry __ get_cache_and_index_at_bcp(rcx, rbx, 1); //获取字段偏移量 __ movptr(rbx, Address(rcx, rbx, Address::times_8, in_bytes(ConstantPoolCache::base_offset() + ConstantPoolCacheEntry::f2_offset())));  //校验rax中实例对象oop,这里没有像getfield一样先把实例对象从栈顶pop到rax中,而是直接校验 //这是因为fast_accessfield类指令的栈顶缓存类型是atos而不是vtos,即上一个指令执行完后会自动将待读取的实例放入rax中 __ verify_oop(rax); __ null_check(rax); Address field(rax, rbx, Address::times_1);  // access field switch (bytecode()) { case Bytecodes::_fast_agetfield: //将属性值拷贝到rax中 __ load_heap_oop(rax, field); __ verify_oop(rax); break; case Bytecodes::_fast_lgetfield: __ movq(rax, field); break; case Bytecodes::_fast_igetfield: __ movl(rax, field); break; case Bytecodes::_fast_bgetfield: __ movsbl(rax, field); break; case Bytecodes::_fast_sgetfield: __ load_signed_short(rax, field); break; case Bytecodes::_fast_cgetfield: __ load_unsigned_short(rax, field); break; case Bytecodes::_fast_fgetfield: __ movflt(xmm0, field); break; case Bytecodes::_fast_dgetfield: __ movdbl(xmm0, field); break; default: ShouldNotReachHere(); }}

我们看到,其实对于volatile变量来说,在读取的时候,并没有做处理。我们来看写操作。

03

 写操作


_putstatic  / _putfield 这两个字节码指令用于写入静态属性或者实例属性,其实现如下:
void TemplateTable::putfield(int byte_no) { putfield_or_static(byte_no, false);} void TemplateTable::putstatic(int byte_no) { putfield_or_static(byte_no, true);} void TemplateTable::putfield_or_static(int byte_no, bool is_static) { transition(vtos, vtos);  const Register cache = rcx; const Register index = rdx; const Register obj = rcx; const Register off = rbx; const Register flags = rax; const Register bc = c_rarg3;  //找到该属性对应的ConstantPoolCacheEntry resolve_cache_and_index(byte_no, cache, index, sizeof(u2)); //发布事件 jvmti_post_field_mod(cache, index, is_static); //获取字段偏移量,flags,如果是静态属性获取对应类的class实例 load_field_cp_cache_entry(obj, cache, index, off, flags, is_static);  Label notVolatile, Done; __ movl(rdx, flags); __ shrl(rdx, ConstantPoolCacheEntry::is_volatile_shift); __ andl(rdx, 0x1);  //....... // btos { //将栈顶的待写入值放入rax中 __ pop(btos); //待写入的值pop出去后,如果是实例属性则栈顶元素为准备写入的实例 //校验该实例是否为空,将其拷贝到obj寄存器中 if (!is_static) pop_and_check_object(obj); //将rax中的待写入值写入到filed地址处 __ movb(field, rax); if (!is_static) { //将该字节码改写成_fast_bputfield,下一次执行时直接执行_fast_bputfield,无需再次判断属性类型 patch_bytecode(Bytecodes::_fast_bputfield, bc, rbx, true, byte_no); } __ jmp(Done); }  __ bind(notByte); //判断是否boolean类型 __ cmpl(flags, ztos); __ jcc(Assembler::notEqual, notBool);  // ztos { __ pop(ztos); if (!is_static) pop_and_check_object(obj); __ andl(rax, 0x1); __ movb(field, rax); if (!is_static) { patch_bytecode(Bytecodes::_fast_zputfield, bc, rbx, true, byte_no); } __ jmp(Done); }  __ bind(notBool); //判断是否引用类型 __ cmpl(flags, atos); __ jcc(Assembler::notEqual, notObj); 
__ bind(notObj); //判断是否int类型 __ cmpl(flags, itos); __ jcc(Assembler::notEqual, notInt); // itos { __ pop(itos); if (!is_static) pop_and_check_object(obj); __ movl(field, rax); if (!is_static) { patch_bytecode(Bytecodes::_fast_iputfield, bc, rbx, true, byte_no); } __ jmp(Done); } __ bind(notInt); //判断是否char类型 __ cmpl(flags, ctos); __ jcc(Assembler::notEqual, notChar); // ctos { __ pop(ctos); if (!is_static) pop_and_check_object(obj); __ movw(field, rax); if (!is_static) { patch_bytecode(Bytecodes::_fast_cputfield, bc, rbx, true, byte_no); } __ jmp(Done); } __ bind(notChar); //判断是否short类型 __ cmpl(flags, stos); __ jcc(Assembler::notEqual, notShort); // stos { __ pop(stos); if (!is_static) pop_and_check_object(obj); __ movw(field, rax); if (!is_static) { patch_bytecode(Bytecodes::_fast_sputfield, bc, rbx, true, byte_no); } __ jmp(Done); } __ bind(notShort); //判断是否long类型 __ cmpl(flags, ltos); __ jcc(Assembler::notEqual, notLong); // ltos { __ pop(ltos); if (!is_static) pop_and_check_object(obj); __ movq(field, rax); if (!is_static) { patch_bytecode(Bytecodes::_fast_lputfield, bc, rbx, true, byte_no); } __ jmp(Done); } __ bind(notLong); //判断是否float类型 __ cmpl(flags, ftos); __ jcc(Assembler::notEqual, notFloat); // ftos { __ pop(ftos); if (!is_static) pop_and_check_object(obj); __ movflt(field, xmm0); if (!is_static) { patch_bytecode(Bytecodes::_fast_fputfield, bc, rbx, true, byte_no); } __ jmp(Done); } __ bind(notFloat); // dtos { //只剩一个,double类型 __ pop(dtos); if (!is_static) pop_and_check_object(obj); __ movdbl(field, xmm0); if (!is_static) { patch_bytecode(Bytecodes::_fast_dputfield, bc, rbx, true, byte_no); } } __ bind(Done); //判断是否volatile变量,如果不是则跳转到notVolatile __ testl(rdx, rdx); __ jcc(Assembler::zero, notVolatile); //如果是 volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad | Assembler::StoreStore)); __ bind(notVolatile);}

同理,我们根据上面的template表,可以看到实际执行的代码段fast_storefield如下。

void TemplateTable::fast_storefield(TosState state) { transition(state, vtos);  ByteSize base = ConstantPoolCache::base_offset(); //发布jvmti时间 jvmti_post_fast_field_mod();  //获取该字段对应的ConstantPoolCacheEntry __ get_cache_and_index_at_bcp(rcx, rbx, 1);  //获取该字段的flags __ movl(rdx, Address(rcx, rbx, Address::times_8, in_bytes(base + ConstantPoolCacheEntry::flags_offset())));  //获取该字段的偏移量 __ movptr(rbx, Address(rcx, rbx, Address::times_8, in_bytes(base + ConstantPoolCacheEntry::f2_offset())));  Label notVolatile; __ shrl(rdx, ConstantPoolCacheEntry::is_volatile_shift); __ andl(rdx, 0x1);  //将待写入的实例对象pop到rcx中,注意此处并没有像putfield一样把待写入的值先pop到rax中, //这是因为fast_storefield类的栈顶缓存类型不是vtos而是具体的写入值类型对应的类型,即上一个//字节码指令执行完成后会自动将待写入的值放入rax中 pop_and_check_object(rcx);  // field address const Address field(rcx, rbx, Address::times_1);  // access field switch (bytecode()) { case Bytecodes::_fast_aputfield: do_oop_store(_masm, field, rax, _bs->kind(), false); break; case Bytecodes::_fast_lputfield: //将rax中的属性值写入到field地址 __ movq(field, rax); break; case Bytecodes::_fast_iputfield: __ movl(field, rax); break; case Bytecodes::_fast_zputfield: __ andl(rax, 0x1); // boolean is true if LSB is 1 // fall through to bputfield case Bytecodes::_fast_bputfield: __ movb(field, rax); break; case Bytecodes::_fast_sputfield: // fall through case Bytecodes::_fast_cputfield: __ movw(field, rax); break; case Bytecodes::_fast_fputfield: __ movflt(field, xmm0); break; case Bytecodes::_fast_dputfield: __ movdbl(field, xmm0); break; default: ShouldNotReachHere(); }  //判断是否volatile变量 __ testl(rdx, rdx); __ jcc(Assembler::zero, notVolatile); volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad | Assembler::StoreStore)); __ bind(notVolatile);}
无论是 fast_storefield还是 putfield_or_static 他们都判断了是否是 volatile变量,然后执行了 volatile_barrier方法。 我们来看一下 volatile_barrier 方法做了什么:

从Hotspot源码分析volatile的实现原理


如果是volatile变量,在属性修改完成后就会执行lock addl $0×0,(%rsp);

lock指令有两个含义:

1、是形成流水线内存屏障,是的流水线执行顺序执行;
2、是的MESI协议中的其他cpu缓存失效,并且将值写入内存;
这样volatile 其他cpu读取的时候就会直接从内存当中加载,而不是使用自己的缓存。


04

 解释器下实现



hotspot解释器模块(hotspot\src\share\vm\interpreter)有两个实现:

  •  C++解释器 : bytecodeInterpreter + cppInterpreter

  • 模板解释器 : templateTable + templateInterpreter


模板解释器我们看完了,下面我们来看一下C++ 解释器的实现。代码位于hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp

从Hotspot源码分析volatile的实现原理

可以看到,在执行完操作之后,bytecodeInterpreter 增加了 OrderAccess::storeload()`屏障,代码如下:


从Hotspot源码分析volatile的实现原理


注意:在java9以后的版本当中,除了之外,都换成了 fence 方法以外,都换成了编译器屏障,如下:

从Hotspot源码分析volatile的实现原理

无论如何 __asm__ volatile ("" : : : "memory"); 和 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory") 完成的效果一样,都是禁止编译器进行优化,并且从内存取数据,而不要从寄存器取数据,如果想了解编译器优化
可以参考我的另一篇博文[ Java内存模型(2)编译器重排序]( https://icefrozen.github.io/article/java-memory-model-complier-reordering/)

3

总结

从Hotspot源码分析volatile的实现原理

总结一下,对于非volatile变量,虽然通过movl等指令修改了某个属性,但是这个修改只是对该CPU所属的高速缓存的修改,并没有实时写回到主内存中,在某个时机下如进程由用户态切换到内核态或者这里的执行lock指令会将对高速缓存行的修改回写到主内存中,同时通过缓存一致性协议通知其他CPU的高速缓存控制器将相关变量的高速缓存行置为无效,当其他CPU再次读取该缓存行时发现该缓存行是无效的,就会重新从主内存加载该变量到高速缓存行中,从而实现对其他CPU的可见性。

你以为就完了,那就太容易,只有理论没有时间自然不能信服,下面我需要用到反汇编指令查看上述的问题。首先,我们用到的linux版本如下

root@ubuntu:~/workplace/javalean/debug/javaclass$ uname -aLinux ubuntu 5.4.0-73-generic #82~18.04.1-Ubuntu SMP Fri Apr 16 15:10:02 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

使用hsdis-amd64.so  首先你需要在网上找一个 hsdis-amd64.so 库,然后放到jdk的目录下/jre/lib/amd66

从Hotspot源码分析volatile的实现原理


编写我们的程序,使得程序可以执行 如下


从Hotspot源码分析volatile的实现原理


编译,并加上打印参数如下

- -XX:+UnlockDiagnosticVMOptions

- -XX:PrintAssembly 打印JIT编译后的汇编

- -XX:CompileCommand=print,*MyClass.myMethod 过滤输出

过滤 add 和add2的方法,这里加上了 -Xcomp 确保是使用模板解释器来进行编译

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=compileonly,*VolatileCode::add* VolatileCode

得到结果如下,add 方法下:

从Hotspot源码分析volatile的实现原理

add2的方法:


对于add 和add2 来说 add 比add多了一个 lock指令。