当前位置: 首页 > 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.jsqmd.com/news/946335/

相关文章:

  • LMDB性能调优实战:从B+树索引到MVCC,如何榨干这个C语言神器的每一分性能
  • 2026 电商运营选型:AI 生成电商短视频的工工具有哪些,哪个最划算?
  • PyTorch张量扩展的底层逻辑:从expand()的‘视图’特性看内存优化与性能陷阱
  • 法院裁定马斯克须在苹果/OpenAI诉讼中提交特斯拉和SpaceX邮件
  • 别再只用map了!Python多进程Pool的apply、starmap实战对比与避坑指南
  • 2026反爬怎么破?从TCP到业务层的6个实战绕过技巧
  • 第1篇_客户端写完了_为什么我还要在PLC里写一个MQTTBroker
  • 数字IC面试官最爱问的Verilog signed问题,除了规则还有这些实战考点
  • 2026年知名的广州番禺专业公司注册/广州番禺极速公司注册/广州番禺高效公司注册老客户推荐 - 品牌宣传支持者
  • 终极指南:DeepSeek-V2-Lite本地部署全流程,单卡40G GPU轻松运行
  • Anylogic智能体建模进阶:手把手教你用‘空间与网络’模块构建动态装备交互仿真
  • 从DB9接头到差分信号:手把手拆解RS232/485/422,搞懂硬件通信的底层逻辑
  • 深入GTX收发器内部:从8B/10B编码到时钟恢复,手把手教你用IBERT进行信号完整性分析
  • Appium Inspector保姆级配置教程:从Desired Capabilities到连接真机/模拟器
  • DeepXDE终极指南:5分钟掌握科学机器学习,让物理方程求解变得简单
  • Multilingual-E5-Large完全指南:如何快速上手多语言文本嵌入模型
  • 数据结构:第2讲:线性表
  • BQ4050电量计I2C通信避坑指南:当芯片手册地址遇上硬件自动左移
  • 计算机毕业设计之基于Python的微博热点新闻舆情分析与可视化
  • Simulink生成DLL时遇到的‘玄学’崩溃?我踩过的坑和终极避坑指南
  • 城市区域火灾概率推演工具:基于贝叶斯网络的Python可运行分析包
  • 从零搭建本地 Hermes Agent,一套整合包搞定自动化智能应用部署
  • 芯片热潮引爆韩国股市跻身全球第六,但泡沫隐忧渐显
  • 2026年10款降AI率平台实测:最高AI率100%直降至0.12%
  • 告别音频接口混乱:用FPGA实现16通道TDM音频传输的保姆级教程(基于48kHz/32bit)
  • 避开Arduino控制好盈电调的三个常见坑:从模拟PWM到定时器中断的优化之路
  • Unity杀戮尖塔风分层地牢生成器:自动布房+智能连通路径Demo
  • 别再乱搜代码了!Arduino Uno控制好盈电调的正确姿势(附寄存器版PWM详解)
  • 告别 Photoshop 插件:纯代码实现 QML 仪表盘的动态变色与交互(附完整工程)
  • STM32F407模拟SMBus读取BQ40Z50电量,我踩过的坑和调试心得(附完整代码)