【Java】volatile关键字
# 【Java】volatile 关键字
# volatile
# 1. 可见性案例
1 | public class ApiTest { |
这段代码,是两个线程操作一个变量,程序期望当 sign
在线程 Thread01 被操作 vt.sign = true
时,Thread02 输出 你坏。
但实际上这段代码永远不会输出 你坏,而是一直处于死循环。这是为什么呢?接下来我们就一步步讲解和验证。
# 2. 加上 volatile 关键字
我们把 sign 关键字加上 volatitle 描述,如下:
1 | class VT implements Runnable { |
测试结果
1 | vt.sign = true 通知 while (!sign) 结束! |
volatile 关键字是 Java 虚拟机提供的的最轻量级的同步机制,它作为一个修饰符出现,用来修饰变量,但是这里不包括局部变量哦
在添加 volatile 关键字后,程序就符合预期的输出了 你坏。从我们对 volatile 的学习认知可以知道。volatile 关键字是 JVM 提供的最轻量级的同步机制,用来修饰变量,用来保证变量对所有线程可见性。
正在修饰后可以让字段在线程见可见,那么这个属性被修改值后,可以及时的在另外的线程中做出相应的反应。
# 3. volatile 怎么保证的可见性
# 3.1 无 volatile 时,内存变化
首先是当 sign 没有 volatitle 修饰时 public boolean sign = false;
,线程 01 对变量进行操作,线程 02 并不会拿到变化的值。所以程序也就不会输出结果 “你坏”
# 3.2 有 volatile 时,内存变化
当我们把变量使用 volatile 修饰时 public volatile boolean sign = false;
,线程 01 对变量进行操作时,会把变量变化的值强制刷新的到主内存。当线程 02 获取值时,会把自己的内存里的 sign 值过期掉,之后从主内存中读取。所以添加关键字后程序如预期输出结果。
# 4. 反编译解毒可见性
类似这样有深度的技术知识,最佳的方式就是深入理解原理,看看它到底做了什么才保证的内存可见性操作。
# 4.1 查看 JVM 指令
指令: javap -v -p VT
1 | public volatile boolean sign; |
从 JVM 指令码中只会发现多了, ACC_VOLATILE
,并没有什么其他的点。所以,也不能看出是怎么实现的可见性。
# 4.2 查看汇编指令
通过 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(opens new window)
- http://vorboss.dl.sourceforge.net/project/fcml/fcml-1.1.1/hsdis-1.1.1-win32-i386.zip(opens new window)
另外是执行命令,包括:
- 基础指令:
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 里,如下图:
配置完成后,不出意外的运行结果如下:
1 | Loaded disassembler from C:\Program Files\Java\jdk1.8.0_161\jre\bin\server\hsdis-amd64.dll |
运行结果就是汇编指令,比较多这里就不都放了。我们只观察🕵重点部分:
1 | 0x0000000003324cda: mov 0x74(%r8),%edx ;*getstatic state |
编译后的汇编指令中,有 volatile 关键字和没有 volatile 关键字,主要差别在于多了一个 lock addl $0x0,(%rsp)
,也就是 lock 的前缀指令。
lock 指令相当于一个内存屏障,它保证如下三点:
- 将本处理器的缓存写入内存。
- 重排序时不能把后面的指令重排序到内存屏障之前的位置。
- 如果是写入动作会导致其他处理器中对应的内存无效。
那么,这里的 1、3 就是用来保证被修饰的变量,保证内存可见性。
# 5. 不加 volatile 也可见吗
1 | 有质疑就要有验证 |
我们现在再把例子修改下,在 while (!sign)
循环体中添加一段执行代码,如下;
1 | class VT implements Runnable { |
修改后去掉了 volatile
关键字,并在 while 循环中添加一段代码。现在的运行结果是:
1 | ... |
咋样,又可见了吧!
这是因为在没 volatile 修饰时,jvm 也会尽量保证可见性。有 volatile 修饰的时候,一定保证可见性。
# 总结
- volatile 会控制被修饰的变量在内存操作上主动把值刷新到主内存,JMM 会把该线程对应的 CPU 内存设置过期,从主内存中读取最新值。
- 那么,volatile 如何防止指令重排也是内存屏障,volatile 的内存屏故障是在读写操作的前后各添加一个 StoreStore 屏障,也就是四个位置,来保证重排序时不能把内存屏障后面的指令重排序到内存屏障之前的位置。
- 另外 volatile 并不能解决原子性问题,即对于复合操作(比如 i++ 等)无法保证线程安全,如果需要解决原子性问题,需要使用 synchronzied 或者 lock。
- 使用 volatile 虽然可以提供比较高的可见性和顺序性的保障,但并不是绝对可靠的,还需要根据具体的业务场景和代码需求来进行综合考虑和评估。
# 关于我
Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!
非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!