创作内容丰富的干货文章很费心力,感谢点过此文章的读者,点一个关注鼓励一下作者,激励他分享更多的精彩好文,谢谢大家!
volatile关键字的作用
volatile 关键字是 JVM 提供的最轻量级的同步机制,用来修饰变量,用来保证变量对所有线程可见性。
volatile怎么保证的可见性
无volatile时,内存变化
首先是当 sign 没有 volatitle 修饰时 public boolean sign = false;,线程01对变量进行操作,线程02并不会拿到变化的值。所以程序也就不会输出结果“你坏”。
有volatile时,内存变化
当我们把变量使用volatile修饰时 public volatile boolean sign = false;,线程01对变量进行操作时,会把变量变化的值强制刷新到主内存。当线程02获取值时,会把自己的内存里的sign值过期掉,之后从主内存中读取。所以添加关键字后程序如预期输出结果。
反编译解读可见性
类似这样有深度的技术知识,最佳的方式就是深入理解原理,看看它到底做了什么才保证的内存可见性操作。
查看JVM指令
指令:javap -v -p VT
public volatile boolean sign; descriptor: Z flags: ACC_PUBLIC, ACC_VOLATILE org.itstack.interview.test.VT(); descriptor: ()V flags: Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_0 6: putfield #2 // Field sign:Z 9: return LineNumberTable: line 35: 0 line 37: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lorg/itstack/interview/test/VT; public void run(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field sign:Z 4: ifne 10 7: goto 0 10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 13: ldc #4 // String 你坏 15: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 18: return LineNumberTable: line 40: 0 line 42: 10 line 43: 18 LocalVariableTable: Start Length Slot Name Signature 0 19 0 this Lorg/itstack/interview/test/VT; StackMapTable: number_of_entries = 2 frame_type = 0 /* same */ frame_type = 9 /* same */ }
从 JVM 指令码中只会发现多了,ACC_VOLATILE,并没有什么其他的点。所以,也不能看出是怎么实现的可见性。
查看汇编指令
通过 Class 文件查看汇编,需要下载 hsdis-amd64.dll 文件,复制到JAVA_HOME\jre\bin\server 目录下。下载资源如下:
-
http://vorboss.dl.sourceforge.net/project/fcml/fcml-1.1.1/hsdis-1.1.1-win32-amd64.zip
-
http://vorboss.dl.sourceforge.net/project/fcml/fcml-1.1.1/hsdis-1.1.1-win32-i386.zip
另外是执行命令,包括:
-
基础指令:java -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
-
指定打印:-XX:CompileCommand=dontinline,类名.方法名
-
指定打印:-XX:CompileCommand=compileonly,类名.方法名
-
输出位置:> xxx
最终使用:java -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=dontinline,ApiTest.main -XX:CompileCommand=compileonly,ApiTest.mian
指令可以在 IDEA 中的 Terminal 里使用,也可以到 DOS 黑窗口中使用
另外,为了更简单的使用,我们把指令可以配置到 idea 的 VM options 里,如下图:
配置完成后,不出意外的运行结果如下:
Loaded disassembler from C:\Program Files\Java\jdk1.8.0_161\jre\bin\server\hsdis-amd64.dll Decoding compiled method 0x0000000003744990: Code: Argument 0 is unknown.RIP: 0x3744ae0 Code size: 0x00000110 [Disassembling for mach='amd64'] [Entry Point] [Constants] # {method} {0x000000001c853d18} 'getSnapshotTransformerList' '()[Lsun/instrument/TransformerManager$TransformerInfo;' in 'sun/instrument/TransformerManager' # [sp+0x40] (sp of caller) 0x0000000003744ae0: mov r10d,dword ptr [rdx+8h] 0x0000000003744ae4: shl r10,3h 0x0000000003744ae8: cmp r10,rax 0x0000000003744aeb: jne 3685f60h ; {runtime_call} 0x0000000003744af1: nop word ptr [rax+rax+0h] 0x0000000003744afc: nop [Verified Entry Point] 0x0000000003744b00: mov dword ptr [rsp+0ffffffffffffa000h],eax 0x0000000003744b07: push rbp 0x0000000003744b08: sub rsp,30h ;*aload_0 ; - sun.instrument.TransformerManag er::getSnapshotTransformerList@0 (line 166) 0x0000000003744b0c: mov eax,dword ptr [rdx+10h] 0x0000000003744b0f: shl rax,3h ;*getfield mTransformerList ; - sun.instrument.TransformerManag er::getSnapshotTransformerList@1 (line 166) 0x0000000003744b13: add rsp,30h ...
运行结果就是汇编指令,比较多这里就不都放了。我们只观察重点部分:
0x0000000003324cda: mov 0x74(%r8),%edx ;*getstatic state; - VT::run@28 (line 27) 0x0000000003324cde: inc %edx 0x0000000003324ce0: mov %edx,0x74(%r8) 0x0000000003324ce4: lock addl $0x0,(%rsp) ;*putstatic state; - VT::run@33 (line 27)
编译后的汇编指令中,有 volatile 关键字和没有 volatile 关键字,主要差别在于多了一个 lock addl $0x0,(%rsp),也就是 lock 的前缀指令。
lock 指令相当于一个内存屏障,它保证如下三点:
1. 将本处理器的缓存写入内存。
2. 重排序时不能把后面的指令重排序到内存屏障之前的位置。
3. 如果是写入动作会导致其他处理器中对应的内存无效。
那么,这里的 1、3 就是用来保证被修饰的变量,保证内存可见性。
不加volatile也可见吗
有质疑就要有验证
我们现在再把例子修改下,在 while (!sign) 循环体中添加一段执行代码,如下:
class VT implements Runnable { public boolean sign = false; public void run() { while (!sign) { System.out.println("你好"); } System.out.println("你坏"); } }
修改后去掉了 volatile 关键字,并在 while 循环中添加一段代码。现在的运行结果是:
你好 你好 你好 vt.sign = true 通知 while (!sign) 结束! 你坏 Process finished with exit code 0
咋样,又可见了吧!
这是因为在没 volatile 修饰时,jvm 也会尽量保证可见性。有 volatile 修饰的时候,一定保证可见性。但可能并非如此,下章节继续深挖,希望可以继续关注我!
总结
-
最后我们再总结下 volatile,它呢,会控制被修饰的变量在内存操作上主动把值刷新到主内存,JMM 会把该线程对应的 CPU 内存设置过期,从主内存中读取最新值。
-
那么,volatile 防止指令重排的方式是内存屏障,volatile 的内存屏故障是在读写操作的前后各添加一个 StoreStore 屏障,StoreStore 屏障是禁止指令重排序的内存屏障,其中Store屏障是每次执行修改数据的时候,强制刷新回主内存。
-
另外 volatile 并不能解决原子性,如果需要解决原子性问题,需要使用synchronzied 或者 lock,这部分内容在我们后续章节中介绍。