原文来自我的博客月泉的博客
食用该篇文章,作者建议你最好已经提前了解过:JMM以及CPU缓存一致性协议还有相关的内存屏障的知识并且能够理解CPU的乱序执行,如果作者理解不当,欢迎指出。 如果本文对你有所帮助不妨给 博客的Github点个小星星,提问和文章有什么地方理解错了也可以直接在Github上提交issue
该篇文章讨论的议题:
- java语义上的volatile
- 内存屏障
- JVM的实现
- 生成的汇编指令
- 如何保障的的可见性和有序性
- 为什么volatile不能保证复合操作的原子性
java语义上的volatile
我们从一个很常见的案例开始出发
public class Test {public static void main(String[] args) throws InterruptedException {Demo demo = new Demo();demo.setName("demo-thread");demo.start();Thread.sleep(1000);demo.flag = false;demo.join();System.out.println(demo.getName() + "线程执行完毕:" + demo.count);}static class Demo extends Thread{boolean flag = true;int count = 0;@Overridepublic void run(){while (flag){count ++;}}}
}
这段代码在执行时,会发生无法停止下来的现象,显而易见,demo-thread
从主内存中读取出flag
的值为true
放在了工作内存中,然后while
去判断该值是否为true
如果为true
就会不断的循环,直至flag
的值为false
为止,然后在main
方法的线程中修改了demo
实例中的flag的值,但demo-thread
线程并没有感知到flag
的值已经被main
线程修改为true
了,从而发生了无法停止的现象,简单点来说这就是线程之间的可见性问题,简单的画个图加深下理解。
那么how to 解决呢?就是我们这篇的主角volatile
(实际上解决的方式有多种,我这里是为了写这篇文章所以这样解决)
public class Test {public static void main(String[] args) throws InterruptedException {...................}static class Demo extends Thread{volatile boolean flag = true;...........}
}
通过volatile
关键字来修饰该变量,使得该变量的修改是对其它线程可见的,同时volatile
还会禁止指令优化的重排序在修饰完volatile
后会在对该变量执行操作时插入内存屏障,在进入下一个议题之前先说说何为内存屏障。
内存屏障
先说说处理器的内存屏障,再来讨论下JVM定义的内存屏障,首先理解内存屏障本质是什么,在本质上的内存屏障实际上就是一类同步屏障指令,加了屏障的地方,假如屏障前有读写操作以及屏障后也有读写操作,那么屏障前的读写操作必然必须先于屏障后的读写操作,屏障后的读写操作也必然必须后于屏障前的读写操作(这里如果理解了乱序执行应该很好理解)
处理器的内存屏障
- read memory barrier (内存读屏障)
保障早于屏障之前的读操作之后再执行晚于屏障的读操作
- write memory barrier(内存写屏障)
保障早于屏障之前的写操作之后再执行晚于屏障的写操作
- full memory barrier(完全内存屏障)
保障早于屏障之前的读写操作之后再执行晚于屏障之后的读写操作
先来认识几个语义的指令,因为待会在JVM的实现中也会发现它的身影,这里简单阐述一下
acquire
: 屏障前的指令不会被排到屏障后去
release
: 屏障后的指令不会被排到屏障前去
fence
:屏障前的指令不会被排到屏障后去,屏障后的指令也不会排到屏障前去
JVM的内存屏障
LoadLoad
读屏障:例如有指令Load1
和Load2
那么假如插入屏障指令为Load1;LoadLoad;Load2
,在中间插入LoadLoad
屏障可以保障读操作不会进行乱序优化,即Load2
在执行时,Load1
的读操作应是执行完了的。
StoreStore
写屏障:例如有指令Store1
和Store2
那么假如插入屏障指令为Store1;StoreSotre;Store2
,在中间插入StoreStore
屏障可以保障写操作不会进行乱序优化,即Store2
在执行时,Store1
的写操作应是执行完了的,并且Sotre1
的写操作是对Store2
可见的,至于为什么可见,我会在说完这四种屏障时给出答案。
LoadStore
读写屏障:例如有指令Load1
和Store2
那么假如插入屏障指令为Load1;LoadStore;Store2
,在中间插入LoadStore
屏障可以保障前面的读操作和后面的写操作不会被乱序优化,即Store2
执行时,Load1
应是执行完了的
StoreLoad
写读屏障:例如有指令Store1
和Load2
那么假如插入屏障的指令为Store1;StoreLoad;Load2
,在中间插入StoreLoad
屏障可以保障前面的写操作对后面的读操作不会被乱序优化,并且是可见的,即Load2
执行时,Store1
应是执行完了的,并其写操作对屏障后的读操作可见
如果无论是加了何种屏障,例如LoadStore
屏障即屏障前的读操作都会从主存中读取值,屏障后的指令会往主存写值,再例如StoreLoad
屏障即屏障前的写操作会往主存写值,从而对屏障后的读操作可见,当然仅仅是往主存写值也不能保障就是可见的,所以后面的读操作也是从主存中读值
JVM的实现
So,现在是不是对volatile
很清晰了,也知道上面为什么加了volatile
关键字后,变量是对其它线程可见的了吧?,从而使得我们的flag
修改后能够在多线程环境下能够有效,以一个非常简单的例子,深入的探讨下去。
public class Demo{static volatile int i;public static void main(String[] args){i = 1;}
}
查看生成的字节码(部分片段)
static volatile int i;descriptor: Iflags: ACC_STATIC, ACC_VOLATILE
可以看到在字节码文件上有一个ACC_VOLATILE
的标识符,接着打开JVM(我用的Hotspot)的代码来看看吧~
可以在JVM源码中看到有一个is_volatile
判断是否是volatile
访问限定符修饰的,然后再看字节码解释器的部分源码
注意有三个细节,首先会判断是否标识了volatile
,然后再判断类型,我们这个是int
类型所以会调用release_int_field_put
,最后插入一道屏障storeload
,首先先看itos
的定义
顾名思义:表示栈顶缓存的int类型数据
接着看release_int_field_put
会发现它调用了OrderAccess::release_store
那么这个方法究竟是干什么的呢?首先注意方法参数添加了volatile
关键字,这是c++的volatile
关键字和Java(java语法也有这个同名的关键字不是吗?)被该关键字所修饰的变量意味着易变的,再c++中修饰了这个关键字的变量每次使用时都会从变量对应的内存地址且编译器也不会对它进行优化。
那么os::atomic_copy64
又是什么呢?这里会针对不同的系统,我这里只看Linux的
粗暴点,就是生成汇编代码去拷贝值吧?
接着我们看
接着看OrderAccess::storeload
请告诉我这4个东西眼熟不眼熟?!?,这当然只是定义不同系统下实现不一样,我们这里还是看linux
的
看这个方法的实现,还有其它三种的实现是什么?本篇文章上面也对该语义进行了阐述吧?继续看linux
下的实现
FULL_MEM_BARRIER
是什么本文上面也说了吧
针对的环境不通实现也不同,这里具体就不再阐述
生成的汇编指令
= = 写到这个小节的时候笔者是用的windows了,所以再补一下windows对fence实现的源码
看下在我机器上生成的汇编代码
[Disassembling for mach='amd64']
[Entry Point]
[Verified Entry Point]
[Constants]# {method} {0x0000000017cf2a38} 'main' '([Ljava/lang/String;)V' in 'org/yuequan/thread/test/Demo'# parm0: rdx:rdx = '[Ljava/lang/String;'# [sp+0x40] (sp of caller)0x00000000037e5320: mov dword ptr [rsp+0ffffffffffffa000h],eax0x00000000037e5327: push rbp0x00000000037e5328: sub rsp,30h0x00000000037e532c: mov rsi,17cf2af8h ; {metadata(method data for {method} {0x0000000017cf2a38} 'main' '([Ljava/lang/String;)V' in 'org/yuequan/thread/test/Demo')}0x00000000037e5336: mov edi,dword ptr [rsi+0dch]0x00000000037e533c: add edi,8h0x00000000037e533f: mov dword ptr [rsi+0dch],edi0x00000000037e5345: mov rsi,17cf2a30h ; {metadata({method} {0x0000000017cf2a38} 'main' '([Ljava/lang/String;)V' in 'org/yuequan/thread/test/Demo')}0x00000000037e534f: and edi,0h0x00000000037e5352: cmp edi,0h0x00000000037e5355: je 37e537eh ;*iconst_1; - org.yuequan.thread.test.Demo::main@0 (line 6)0x00000000037e535b: mov rsi,0d5b0dad0h ; {oop(a 'java/lang/Class' = 'org/yuequan/thread/test/Demo')}0x00000000037e5365: mov edi,1h0x00000000037e536a: mov dword ptr [rsi+68h],edi0x00000000037e536d: lock add dword ptr [rsp],0h ;*putstatic i; - org.yuequan.thread.test.Demo::main@1 (line 6)0x00000000037e5372: add rsp,30h0x00000000037e5376: pop rbp0x00000000037e5377: test dword ptr [2f20100h],eax; {poll_return}0x00000000037e537d: ret0x00000000037e537e: mov qword ptr [rsp+8h],rsi0x00000000037e5383: mov qword ptr [rsp],0ffffffffffffffffh0x00000000037e538b: call 37e20a0h ; OopMap{rdx=Oop off=112};*synchronization entry; - org.yuequan.thread.test.Demo::main@-1 (line 6); {runtime_call}0x00000000037e5390: jmp 37e535bh0x00000000037e5392: nop0x00000000037e5393: nop0x00000000037e5394: mov rax,qword ptr [r15+2a8h]0x00000000037e539b: mov r10,0h0x00000000037e53a5: mov qword ptr [r15+2a8h],r100x00000000037e53ac: mov r10,0h0x00000000037e53b6: mov qword ptr [r15+2b0h],r100x00000000037e53bd: add rsp,30h0x00000000037e53c1: pop rbp0x00000000037e53c2: jmp 374ece0h ; {runtime_call}0x00000000037e53c7: hlt0x00000000037e53c8: hlt0x00000000037e53c9: hlt0x00000000037e53ca: hlt0x00000000037e53cb: hlt0x00000000037e53cc: hlt0x00000000037e53cd: hlt0x00000000037e53ce: hlt0x00000000037e53cf: hlt0x00000000037e53d0: hlt0x00000000037e53d1: hlt0x00000000037e53d2: hlt0x00000000037e53d3: hlt0x00000000037e53d4: hlt0x00000000037e53d5: hlt0x00000000037e53d6: hlt0x00000000037e53d7: hlt0x00000000037e53d8: hlt0x00000000037e53d9: hlt0x00000000037e53da: hlt0x00000000037e53db: hlt0x00000000037e53dc: hlt0x00000000037e53dd: hlt0x00000000037e53de: hlt0x00000000037e53df: hlt
[Exception Handler]
[Stub Code]0x00000000037e53e0: call 3750aa0h ; {no_reloc}0x00000000037e53e5: mov qword ptr [rsp+0ffffffffffffffd8h],rsp0x00000000037e53ea: sub rsp,80h0x00000000037e53f1: mov qword ptr [rsp+78h],rax0x00000000037e53f6: mov qword ptr [rsp+70h],rcx0x00000000037e53fb: mov qword ptr [rsp+68h],rdx0x00000000037e5400: mov qword ptr [rsp+60h],rbx0x00000000037e5405: mov qword ptr [rsp+50h],rbp0x00000000037e540a: mov qword ptr [rsp+48h],rsi0x00000000037e540f: mov qword ptr [rsp+40h],rdi0x00000000037e5414: mov qword ptr [rsp+38h],r80x00000000037e5419: mov qword ptr [rsp+30h],r90x00000000037e541e: mov qword ptr [rsp+28h],r100x00000000037e5423: mov qword ptr [rsp+20h],r110x00000000037e5428: mov qword ptr [rsp+18h],r120x00000000037e542d: mov qword ptr [rsp+10h],r130x00000000037e5432: mov qword ptr [rsp+8h],r140x00000000037e5437: mov qword ptr [rsp],r150x00000000037e543b: mov rcx,6601c4e0h ; {external_word}0x00000000037e5445: mov rdx,37e53e5h ; {internal_word}0x00000000037e544f: mov r8,rsp0x00000000037e5452: and rsp,0fffffffffffffff0h0x00000000037e5456: call 65cd4510h ; {runtime_call}0x00000000037e545b: hlt
[Deopt Handler Code]0x00000000037e545c: mov r10,37e545ch ; {section_word}0x00000000037e5466: push r100x00000000037e5468: jmp 3727600h ; {runtime_call}0x00000000037e546d: hlt0x00000000037e546e: hlt0x00000000037e546f: hlt
Decoding compiled method 0x00000000037e4ed0:
Code:
Argument 0 is unknown.RIP: 0x37e5020 Code size: 0x00000110
[Entry Point]
[Verified Entry Point]
[Constants]# {method} {0x0000000017cf2a38} 'main' '([Ljava/lang/String;)V' in 'org/yuequan/thread/test/Demo'# parm0: rdx:rdx = '[Ljava/lang/String;'# [sp+0x40] (sp of caller)0x00000000037e5020: mov dword ptr [rsp+0ffffffffffffa000h],eax0x00000000037e5027: push rbp0x00000000037e5028: sub rsp,30h ;*iconst_1; - org.yuequan.thread.test.Demo::main@0 (line 6)0x00000000037e502c: mov rsi,0d5b0dad0h ; {oop(a 'java/lang/Class' = 'org/yuequan/thread/test/Demo')}0x00000000037e5036: mov edi,1h0x00000000037e503b: mov dword ptr [rsi+68h],edi0x00000000037e503e: lock add dword ptr [rsp],0h ;*putstatic i; - org.yuequan.thread.test.Demo::main@1 (line 6)0x00000000037e5043: add rsp,30h0x00000000037e5047: pop rbp0x00000000037e5048: test dword ptr [2f20100h],eax; {poll_return}0x00000000037e504e: ret0x00000000037e504f: nop0x00000000037e5050: nop0x00000000037e5051: mov rax,qword ptr [r15+2a8h]0x00000000037e5058: mov r10,0h0x00000000037e5062: mov qword ptr [r15+2a8h],r100x00000000037e5069: mov r10,0h0x00000000037e5073: mov qword ptr [r15+2b0h],r100x00000000037e507a: add rsp,30h0x00000000037e507e: pop rbp0x00000000037e507f: jmp 374ece0h ; {runtime_call}0x00000000037e5084: hlt0x00000000037e5085: hlt0x00000000037e5086: hlt0x00000000037e5087: hlt0x00000000037e5088: hlt0x00000000037e5089: hlt0x00000000037e508a: hlt0x00000000037e508b: hlt0x00000000037e508c: hlt0x00000000037e508d: hlt0x00000000037e508e: hlt0x00000000037e508f: hlt0x00000000037e5090: hlt0x00000000037e5091: hlt0x00000000037e5092: hlt0x00000000037e5093: hlt0x00000000037e5094: hlt0x00000000037e5095: hlt0x00000000037e5096: hlt0x00000000037e5097: hlt0x00000000037e5098: hlt0x00000000037e5099: hlt0x00000000037e509a: hlt0x00000000037e509b: hlt0x00000000037e509c: hlt0x00000000037e509d: hlt0x00000000037e509e: hlt0x00000000037e509f: hlt
[Exception Handler]
[Stub Code]0x00000000037e50a0: call 3750aa0h ; {no_reloc}0x00000000037e50a5: mov qword ptr [rsp+0ffffffffffffffd8h],rsp0x00000000037e50aa: sub rsp,80h0x00000000037e50b1: mov qword ptr [rsp+78h],rax0x00000000037e50b6: mov qword ptr [rsp+70h],rcx0x00000000037e50bb: mov qword ptr [rsp+68h],rdx0x00000000037e50c0: mov qword ptr [rsp+60h],rbx0x00000000037e50c5: mov qword ptr [rsp+50h],rbp0x00000000037e50ca: mov qword ptr [rsp+48h],rsi0x00000000037e50cf: mov qword ptr [rsp+40h],rdi0x00000000037e50d4: mov qword ptr [rsp+38h],r80x00000000037e50d9: mov qword ptr [rsp+30h],r90x00000000037e50de: mov qword ptr [rsp+28h],r100x00000000037e50e3: mov qword ptr [rsp+20h],r110x00000000037e50e8: mov qword ptr [rsp+18h],r120x00000000037e50ed: mov qword ptr [rsp+10h],r130x00000000037e50f2: mov qword ptr [rsp+8h],r140x00000000037e50f7: mov qword ptr [rsp],r150x00000000037e50fb: mov rcx,6601c4e0h ; {external_word}0x00000000037e5105: mov rdx,37e50a5h ; {internal_word}0x00000000037e510f: mov r8,rsp0x00000000037e5112: and rsp,0fffffffffffffff0h0x00000000037e5116: call 65cd4510h ; {runtime_call}0x00000000037e511b: hlt
[Deopt Handler Code]0x00000000037e511c: mov r10,37e511ch ; {section_word}0x00000000037e5126: push r100x00000000037e5128: jmp 3727600h ; {runtime_call}0x00000000037e512d: hlt0x00000000037e512e: hlt0x00000000037e512f: hlt
这么长肿么看?看关键部位就好拉
看到是使用的lock
指令吧?那么问题来了lock
指令是什么,我在这里肤浅的解释一下:CPU提供了在执行指令期间提供了总线加锁的手段,那么加了lock
的汇编生成机器码就使CPU在执行这条指令的时候会把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,从而保证这条指令执行的原子性
如何保障的的可见性和有序性
通过内存屏障来提醒编译器和CPU不对指令进行优化防止其优化乱序执行从而达到有序性,在屏障的前后都是通过主存读写达到线程之间的可见性。(O(∩_∩)O 我想不用再多解释了)
为什么volatile不能保证复合操作的原子性
就比如在多线程中,多个线程对于一个实例变量的变量i
进行自增操作,例如i++
,这时候就产生了竞态条件导致,例如给i
变量加5000次,得到的结果可能是5000得到的结果也可能是少于5000,尽管你加了volatile
,这是为什么呢?要知道volatile
仅是通过内存屏障的机制来保障
例如
load1;load2;store1;store2;StoreLoad;load3;store3.....
尽管你保证了可见性,但你不能保证该操作的原子性,所谓的原子性本质就是指令在执行期间不被打断,要么就是不执行,仔细想想i++
是不是一个三步的复合操作:取值、相加、赋值,就比如:你在未赋值时别的线程执行时,别的线程也正在处以赋值状态,语言解释好麻烦看下列示例
i= 0
线程A 线程B
取值 0取值 0
相加 1 相加 1
赋值 1 赋值 1
尽管你保障了可见性,但你并不能保证你拿到手上的值永远是最新值。