掉了两根头发,可算是把volatile整明白了

简介: 为什么只能保证可见性?又是怎么实现禁用指令重排?哇,原来这么简单

本来想着快过年了偷个懒休息下,没想到被兄弟们连续催更,没办法,博主暖男嘛,掐着人中也要更,兄弟们卷起来


volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但对于为什么它只能保证可见性,不保证原子性,它又是如何禁用指令重排的,还有很多同学没彻底理解


相信我,坚持看完这篇文章,你将牢牢掌握一个Java核心知识点,原文链接


先说它的两个作用:

  • 保证变量在内存中对线程的可见性
  • 禁用指令重排


每个字都认识,凑在一起就麻了


这两个作用通常很不容易被我们Java开发人员正确、完整地理解,以至于许多同学不能正确地使用volatile


关于可见性


不多bb,码来


publicclassVolatileTest {
   privatestaticvolatileint count = 0;

   privatestaticvoidincrease() {
       count++;
   }

   publicstaticvoidmain(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++这个操作不是原子的,它是分三步进行

  1. 从内存读取 count 的值
  2. 执行 count + 1
  3. 将 count 的新值写回


要彻底搞懂这个问题,我们得从字节码入手

下面是increase方法编译后的字节码



看不懂没关系,我们一行一行来看:

  1. GETSTATIC:读取 count 的当前值
  2. ICONST_1:将常量 1 加载到栈顶
  3. IADD:执行+1
  4. PUTSTATIC:写入count最新值


ICONST_1和IADD其实就是真正的++操作


关键点来了,volatile只能保证线程在GETSTATIC这一步拿到的值是最新的,但当该线程执行到下面几行指令时,这期间可能就有其它线程把count的值修改了,最终导致旧值把真正的新值覆盖


懂我意思吗


所以,并发编程中,只靠volatile修饰共享变量是不可靠的,最终还是要通过对关键方法加锁来保证线程安全


就如上面的demo,稍加修改就能实现真正的线程安全


最简单的,给increase方法加个synchronized (synchronized怎么实现线程安全的我就不啰嗦了,我以前讲过 synchronized底层实现原理


   privatesynchronizedstaticvoidincrease() {
       ++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代码明明是从上往下依次执行,为什么会出现指令重排这个问题?


ok我话说完

相关文章
|
存储 人工智能 数据可视化
Serverless Devs 介绍
## 开篇 2020年11月份,阿里云智能开源了Serverless 社区的开发者工具Serverless Devs(后简称S) 弥补了国内在Serverless 开发者工具的一个空白。通过高度灵活的配置设定,实现了无厂商锁定的支持;直观易懂的可视化配套也带来了极致的开发者使用体验。通过S你可以体验Serverless hello world 以及 构建生产级Serverless 应用 。你
1972 0
|
3月前
|
人工智能 自然语言处理 安全
OpenClaw 与飞书生态对接指南:企业IM+AI高效集成实操教程
本文为OpenClaw与飞书生态对接的实操指南,涵盖前期筹备(权限、JDK环境、凭证管理)、飞书凭证获取、OpenClaw后台配置、安装包下载排错及常见异常排查,步骤详实、安全规范,助力企业快速实现IM+AI无缝集成。
|
4月前
|
弹性计算 数据安全/隐私保护
阿里云怎样部署我的世界(MC)服务器?2026年最新攻略来了!
阿里云怎样部署我的世界(MC)服务器?阿里云推出了我的世界(MC)一键部署方案,无需专业技术,新手小白也能快速部署联机游戏服务器!
1084 10
|
7月前
|
消息中间件 算法 API
三大电商API应用对比:淘宝京东拼多多谁能笑到最后?
对比主流电商平台API:淘宝架构稳定、安全性高,适合全链路整合;京东物流性能领先,适合高效物流场景;拼多多社交裂变能力强,适配社交电商。结合分层架构、微服务与事件驱动,建议按业务需求选择。
|
监控 负载均衡 安全
CC攻击来袭?手把手教你打造云服务器防护盾
在网络攻击频发的今天,CC攻击如同数字世界的“洪水猛兽”,能迅速压垮云服务器。作为站长,了解CC攻击原理并掌握防御策略至关重要。本文从认识CC攻击出发,详细介绍了七大防护措施:CDN加速、智能防火墙、连接数限制、验证码机制、WAF防护、负载均衡及监控告警系统。同时提供进阶防护方案,如高防IP和应急响应计划。
459 3
|
存储 缓存 安全
可重入函数与不可重入函数
可重入函数与不可重入函数
819 0
|
Oracle 关系型数据库 Linux
VMware的竞品
VMware的竞品
712 2
|
数据库管理 数据库 安全
sqlite事务模型、性能优化tips、常见误区
sqlite事务模型、性能优化tips、常见误区
10456 156
|
消息中间件 存储 运维
阿里云消息队列 RocketMQ 5.0 全新升级:消息、事件、流融合处理平台
RocketMQ5.0 的发布标志着阿里云消息从消息领域正式迈向了“消息、事件、流”场景大融合的新局面。未来阿里云消息产品的演进也将继续围绕消息、事件、流核心场景而开展。
1768 1
阿里云消息队列 RocketMQ 5.0 全新升级:消息、事件、流融合处理平台
|
监控 网络协议 算法
TCP 拥塞控制详解 | 6. 主动队列管理
TCP 拥塞控制详解 | 6. 主动队列管理
1008 1
TCP 拥塞控制详解 | 6. 主动队列管理