当前位置: 首页 > news >正文

JMM、volatile 与 CAS:并发安全三大问题

并发程序为什么会出问题?

很多人会先说“因为多线程同时执行”。这句话没错,但太粗了。真正落到 Java 面试里,通常要拆成三个词:原子性、可见性、有序性。

synchronizedLockvolatile、CAS、Atomic 类,本质上都是围绕这三个问题在做不同取舍。

并发安全到底在防什么

PPT 里把 Java 并发编程三大特性列得很清楚:

特性问题常见解决方式
原子性一组操作执行到一半被别的线程插进来synchronizedLock、CAS、Atomic 类
可见性一个线程改了共享变量,另一个线程看不到volatilesynchronizedLock
有序性编译器或 CPU 为优化执行顺序,导致多线程结果异常volatile、锁、happens-before 规则

并发程序出问题

原子性

可见性

有序性

共享变量操作被打断

线程本地缓存没有及时同步

指令重排改变多线程观察结果

synchronized / Lock / CAS

volatile / synchronized / Lock

volatile 内存屏障 / 锁语义

原子性:ticketNum-- 不是一步

下面这种扣库存逻辑,单线程下没问题,多线程下就危险:

intticketNum=10;publicvoidgetTicket(){if(ticketNum<=0){return;}System.out.println(Thread.currentThread().getName()+" 抢到一张票, 剩余:"+ticketNum);ticketNum--;}

ticketNum--看起来是一行代码,实际不是一个不可分割的动作。它至少包含:

  1. 读取ticketNum
  2. 计算ticketNum - 1
  3. 写回ticketNum

两个线程可能同时读到1,然后都扣减成功。

主内存 ticketNum线程 T2线程 T1主内存 ticketNum线程 T2线程 T1读取 ticketNum = 1读取 ticketNum = 1写回 0写回 0

解决原子性最直接的方式是加锁:

publicsynchronizedvoidgetTicket(){if(ticketNum<=0){return;}ticketNum--;}

也可以用Lock,或者在适合的场景下用 Atomic 类底层的 CAS。

JMM 是什么

JMM,全称 Java Memory Model,Java 内存模型。

它不是 JVM 内存结构里的堆、栈、方法区那套东西。JMM 讨论的是:多线程读写共享变量时,Java 语言层面应该遵守什么规则。

JMM 把内存抽象成两块:

  1. 主内存,保存共享变量。
  2. 工作内存,每个线程自己的本地副本。

线程之间不能直接访问对方的工作内存。线程 A 要把修改告诉线程 B,必须通过主内存完成。

不能直接通信

主内存
共享变量

线程 A 工作内存
变量副本

线程 B 工作内存
变量副本

这就引出了可见性问题:线程 A 修改了共享变量,但线程 B 可能还在用自己的旧副本。

volatile 解决什么

volatile有两层核心语义:

  1. 保证线程间可见性。
  2. 禁止特定指令重排序。

先看可见性:

privatestaticvolatilebooleanstop=false;publicstaticvoidmain(String[]args){newThread(()->{while(!stop){// busy loop}System.out.println("stopped");},"t1").start();newThread(()->{stop=true;},"t2").start();}

如果stop不加volatile,线程t1可能一直读不到t2写入的新值。JIT 编译器还可能把循环优化得更激进,让结果更难预测。

加了volatile后,写线程对stop的修改会对读线程可见。

读线程主内存 stop写线程读线程主内存 stop写线程volatile 写 stop = truevolatile 读看到新值跳出循环

但要注意,volatile不保证复合操作的原子性。

下面这样仍然不安全:

volatileintcount=0;publicvoidincrement(){count++;}

count++还是读、改、写三步。volatile能保证每次读写的可见性,但不能把三步合成一个原子操作。

volatile 怎么禁止重排序

CPU 和编译器为了性能,可能会调整指令顺序。单线程下只要最终结果一致就行,但多线程下,其他线程可能观察到中间状态。

PPT 里用 jcstress 做了一个例子:

intx;inty;@Actorpublicvoidactor1(){x=1;y=1;}@Actorpublicvoidactor2(II_Resultr){r.r1=y;r.r2=x;}

如果出现r1 = 1, r2 = 0,就说明线程 2 看到了y = 1,却没看到x = 1。这在直觉上很奇怪,因为代码里x = 1写在y = 1前面。

给关键变量加volatile,JVM 会在 volatile 读写附近插入内存屏障,限制重排序。

阻止前面的普通写跑到 volatile 写后面

阻止后面的普通读跑到 volatile 读前面

x = 1 普通写

volatile 写 y = 1

写屏障

volatile 读 y

读屏障

读 x

使用技巧可以简单记:

写变量时,让volatile变量尽量放在发布动作的最后。

读变量时,让volatile变量尽量放在读取动作的最前。

这不是死规矩,但有助于理解“用一个 volatile 变量作为状态发布点”的模式。

CAS 是什么

CAS,全称 Compare And Swap,比较并交换。

它体现的是乐观锁思想:先不加互斥锁,假设竞争不严重。更新时比较一下共享变量现在的值是不是自己当初看到的旧值,如果是,就更新;如果不是,说明被别人改过,那就重试。

CAS 有三个核心值:

名称含义
V当前内存值
A旧的预期值
B准备更新的新值

只有当V == A时,才把值改成B

读取当前值 A

计算新值 B

当前内存值 V 是否等于 A

更新为 B

更新失败

重新读取并自旋重试

用伪代码表示就是:

while(true){intoldValue=value;intnewValue=oldValue+1;if(compareAndSwap(oldValue,newValue)){break;}}

CAS 底层通常依赖 CPU 原子指令,Java 里很多并发工具都会用到,比如 Atomic 类、AQS 等。

CAS 和 synchronized 怎么选

PPT 里用了一个很口语化但很好记的对比:

synchronized是悲观锁,想的是“别人一定会来改,我先锁住”。

CAS 是乐观锁,想的是“别人不一定来改,就算改了我再重试”。

对比点synchronized / LockCAS
思想悲观锁乐观锁
线程状态竞争失败可能阻塞竞争失败通常自旋重试
适合场景临界区较大、竞争激烈、逻辑复杂临界区很小、冲突不高
风险阻塞和唤醒有成本高竞争下自旋浪费 CPU

所以不要把 CAS 神化。竞争很低时 CAS 很漂亮,竞争很高时大量线程一直自旋,也会把 CPU 打满。

面试怎么答

可以这么组织:

并发程序出问题的根本原因主要是原子性、可见性、有序性。

原子性指一组操作不能被中途打断,比如i++不是原子操作,可以用synchronizedLock或 Atomic 类解决。可见性指一个线程对共享变量的修改,另一个线程能否及时看到,volatile、锁都能保证可见性。有序性指编译器和 CPU 可能重排指令,多线程下可能观察到异常结果,volatile可以通过内存屏障限制重排序。

JMM 定义了多线程读写共享变量的规则,线程有自己的工作内存,线程间通信必须经过主内存。

CAS 是比较并交换,是一种乐观锁思想。它比较当前内存值和旧预期值,如果一致就更新,否则自旋重试。Atomic 类和 AQS 都大量使用 CAS。CAS 适合冲突较少、操作很短的场景;竞争激烈时,自旋重试也会带来性能问题。

http://www.gsyq.cn/news/1457822.html

相关文章:

  • 数字IC面试官最爱问的Verilog signed问题,除了规则还有这些实战考点
  • 2026年知名的广州番禺专业公司注册/广州番禺极速公司注册/广州番禺高效公司注册老客户推荐 - 品牌宣传支持者
  • DeepXDE终极指南:5分钟掌握科学机器学习,让物理方程求解变得简单
  • 计算机毕业设计之基于Python的微博热点新闻舆情分析与可视化
  • 芯片热潮引爆韩国股市跻身全球第六,但泡沫隐忧渐显
  • 2026年10款降AI率平台实测:最高AI率100%直降至0.12%
  • 磁盘寻道时间计算与调度算法(FCFS、SSTF、SCAN、C-SCAN)
  • 示波器函数/任意波形发生器直流电源 | SiC/GaN 宽禁带半导体器件动态特性测试
  • 计算机毕业设计之基于推荐的系统的新闻阅读平台的设计与实现
  • WinCC数据备份避坑指南:用VBS脚本搞定OnlineTableControl周期性导出CSV(附解决‘文件已存在’弹窗方法)
  • 避坑指南:Verilog写BMP图片时多出0D字节?详解‘wb+’与‘w+’模式的区别
  • 保姆级教程:在ROS1/ROS2中配置AMCL参数,让机器人定位又快又准
  • 大数据量高并发的数据库优化
  • unity项目文件拷贝
  • 3分钟掌握百度文库文档纯净打印技巧:告别广告干扰,专注内容获取
  • 别再为缺失的交通数据发愁了!手把手教你用Python实现TAS-LR时空数据重建
  • Switch 2 屏幕保护膜推荐:多款产品对比,总有一款适合你!
  • 告别CH340!用STM32F103C8T6的USB虚拟串口实现稳定通信(附完整工程源码)
  • 别再浪费性能了!ESXi硬盘控制器直通实战,让虚拟机磁盘IO飞起来
  • 2026年知名的深圳整厂打包回收/广东整厂设施拆除回收/广东整厂冲床回收优质公司推荐 - 行业平台推荐
  • 别再手动编TLE了!用MATLAB+STK批量生成卫星轨道根数的保姆级脚本
  • 保姆级教程:在Ubuntu 20.04 + ROS Noetic下,用Realsense D435i搞定UR3机械臂手眼标定
  • Multi-Agent系统日志分析:智能体行为追溯与问题排查
  • CVE-2026-0826深度解析:CVSS9.2 HP Poly全网VoIP未认证RCE,企业内网最大隐形炸弹
  • 2026年质量好的嘉创排烟窗/圆拱型排烟窗/三角型排烟窗实力工厂推荐 - 品牌宣传支持者
  • 深入Photon OS:揭秘VCSA克隆恢复后,5480界面背后的服务依赖与启动逻辑
  • A2A协议深度解析(流式返回以及多agent协同)
  • 把ESP32-CAM变成智能门铃:低成本实现局域网视频监控与人脸识别告警
  • 25级数应四班第六次实验
  • 从蓝牙到Wi-Fi:拆解FSK、PSK、QAM在常见物联网协议中的真实应用