java并发编程关键字volatile保证可见性不保证原子性详解
目录
- 关于可见性
- 关于指令重排
相信我,坚持看完这篇文章,你将牢牢掌握一个Java核心知识点
先说它的两个作用:
保证变量在内存中对线程的可见性禁用指令重排
每个字都认识,凑在一起就麻了
这两个作用通常很不容易被我们Java开发人员正确、完整地理解,以至于许多同学不能正确地使用volatile
关于可见性 不多bb,码来
public class VolatileTest {private static volatile int count = 0; private static void increase() {count++; }public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 10; i++) {new Thread(() -> {for (int j = 0; j < 10000; j++) {increase(); }}).start(); }// 所有线程累加完成后输出while (Thread.activeCount() > 2) Thread.yield(); System.out.println(count); }}
代码很好理解,开了十个线程对同一个共享变量count做累加,每个线程累加1w次
count我们已经用volatile修饰,已经保证了count对十个线程在内存中的可见性,按理说十个线程执行完毕count的值应该10w
然鹅,运行多次,结果都远小于期望值
文章图片
是哪个环节出了问题?
文章图片
你肯定听过一句话:volatile只保证可见性,不保证原子性
这句话就是答案,但是依旧很多人没搞懂其中的奥秘
说来话长我长话短说,简单来讲就是 count++这个操作不是原子的,它是分三步进行
- 从内存读取 count 的值
- 执行 count + 1
- 将 count 的新值写回
下面是increase方法编译后的字节码
文章图片
看不懂没关系,我们一行一行来看:
- GETSTATIC:读取 count 的当前值
- ICONST_1:将常量 1 加载到栈顶
- IADD:执行+1
- PUTSTATIC:写入count最新值
关键点来了,volatile只能保证线程在GETSTATIC这一步拿到的值是最新的,但当该线程执行到下面几行指令时,这期间可能就有其它线程把count的值修改了,最终导致旧值把真正的新值覆盖
懂我意思吗
所以,并发编程中,只靠volatile修饰共享变量是不可靠的,最终还是要通过对关键方法加锁来保证线程安全
就如上面的demo,稍加修改就能实现真正的线程安全
最简单的,给increase方法加个synchronized (synchronized怎么实现线程安全的我就不啰嗦了,我以前讲过 synchronized底层实现原理)
private synchronized static void increase() {++count; }
run几下
文章图片
这不就妥了嘛
到现在,对于以下两点你应该有了新的认知
volatile保证变量在内存中对线程的可见性
volatile只保证可见性,不保证原子性
关于指令重排 并发编程中,cpu自身和虚拟机为了提高执行效率,都会采用指令重排(在保证不影响结果的前提下,将某些代码乱序执行)
- 关于cpu:为了从分利用cpu,实际执行指令时会做优化;
- 关于虚拟机:在HotSpot vm中,为了提升执行效率,JIT(即时编译)模式也会做指令优化
文章图片
伪代码取自《深入理解Java虚拟机》:
其描述的场景是开发中常见配置读取过程,只是我们在处理配置文件时一般不会出现并发,所以没有察觉这会有问题。
试想一下,如果定义initialized变量时没有使用volatile修饰,就可能会由于指令重排序的优化,导致位于线程A中最后一条代码“initialized=true”被提前执行(这里虽然使用Java作为伪代码,但所指的重排序优化是机器级的优化操作,提前执行是指这条语句对应的汇编代码被提前执行),这样在线程B中使用配置信息的代码就可能出现错误,而volatile通过禁止指令重排则可以避免此类情况发生
禁用指令重排只需要将变量声明为volatile,是不是很神奇
我们来看看volatile是如何实现禁用指令重排的
也借用《深入理解Java虚拟机》的一个例子吧,比较好理解
文章图片
这是个单例模式的实现,下面是它的部分字节码,红框中 mov%eax,0x150(%esi) 是对instance赋值
文章图片
可以看到,在赋值后,还执行了 lock addl$0x0,(%esp) 指令,关键点就在这儿,这行指令相当于此处设置了个 内存屏障 ,有了内存屏障后,cpu或虚拟机在指令重排时就不能把内存屏障后面的指令提前到内存屏障前面,好好捋一下这段话
最后,留一个能加深大家对volatile理解的问题,兄弟们好好思考下:
Java代码明明是从上往下依次执行,为什么会出现指令重排这个问题?
【java并发编程关键字volatile保证可见性不保证原子性详解】以上就是java并发编程关键字volatile保证可见性不保证原子性详解的详细内容,更多关于java并发编程关键字volatile的资料请关注脚本之家其它相关文章!
推荐阅读
- Java|Java ConcurrentModificationException异常解决案例详解
- Java提效神器Stream的一些冷门技巧汇总
- Java学习笔记|java网络编程基础详解三之TCP协议的3次握手与4次挥手过程详解
- 计算机网络|TCP网络编程模型从入门到实战基础篇,单服务器单个用户非并发版本
- java|Java 面试八股文之基础篇(一)
- JavaWeb|多线程案例
- JavaWeb|网络通信基础
- JavaWeb|Java网络编程基础
- JavaWeb|HTML 基础
- 【Go进阶—并发编程】Context