Java 并发进阶:JMM、volatile、CAS、Atomic、锁升级、ReentrantLock 一篇讲清
前面我们聊了 Java 并发基础,比如进程、线程、线程状态、synchronized 和 volatile 的基本区别。
这一篇继续往下走,进入 Java 并发里更核心的一组内容:
- JMM 是什么?
- 主内存和工作内存是什么?
volatile为什么能保证可见性?- 什么是指令重排序?
- happens-before 是什么?
- CAS 是什么?
- ABA 问题怎么理解?
AtomicInteger和volatile int有什么区别?LongAdder为什么高并发下更快?synchronized锁升级是什么?ReentrantLock和synchronized有什么区别?
这些都是 Java 后端面试里非常高频的并发问题。
一、什么是 JMM?
JMM 全称是 Java Memory Model,也就是 Java 内存模型。
注意,它不是 JVM 内存结构里的堆、栈、方法区。
JMM 是一套并发规范,用来规定:
线程如何读写共享变量
线程之间如何保证可见性
哪些操作不能随便重排序
简单理解:
JMM 主要解决多线程下的可见性和有序性问题。
二、主内存和工作内存是什么?
JMM 抽象出两个概念:
| 概念 | 含义 |
|---|---|
| 主内存 | 共享变量实际存放的地方 |
| 工作内存 | 每个线程自己的变量副本 |
图示:

线程操作共享变量时,通常会先从主内存拷贝到自己的工作内存,再进行操作。
这就可能导致一个问题:
一个线程修改了变量,另一个线程不一定马上看到。
三、为什么会有可见性问题?
比如有一个变量:
boolean flag = true;
线程 A 执行:
flag = false;
线程 B 执行:
while (flag) {
// do something
}
如果没有 volatile 或锁,线程 B 可能一直读自己工作内存中的旧值 true,看不到线程 A 已经把它改成了 false。
这就是可见性问题。
四、volatile 如何保证可见性?
使用 volatile 修饰变量:
private volatile boolean flag = true;
它的效果是:
线程写 volatile 变量时,会把值刷新到主内存;
线程读 volatile 变量时,会从主内存读取最新值。
所以一个线程修改后,其他线程能及时看到。
面试简洁说法:
volatile 通过内存屏障保证变量修改对其他线程可见。
五、什么是指令重排序?
为了提高性能,编译器和 CPU 可能会调整指令执行顺序。
比如代码顺序是:
1. a = 1
2. b = 2
实际执行时,只要不影响单线程结果,可能变成:
1. b = 2
2. a = 1
单线程看起来没问题,但多线程下可能出错。
所以并发里不仅要关注可见性,还要关注有序性。
六、volatile 如何禁止指令重排序?
volatile 会插入内存屏障,限制特定指令前后不能随意重排。
最经典的场景是单例模式的双重检查锁定:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
如果没有 volatile,对象创建过程可能发生重排序,其他线程可能拿到一个“还没初始化完成”的对象。
所以 DCL 单例里,instance 通常要加 volatile。
七、什么是 happens-before?
happens-before 是 JMM 中判断可见性的规则。
如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见。
常见规则:
| 规则 | 含义 |
|---|---|
| 程序顺序规则 | 同一线程内,前面的操作先于后面的操作 |
| volatile 规则 | 写 volatile 先于后续读 volatile |
| 锁规则 | 解锁先于后续加锁 |
| 线程启动规则 | start() 先于线程内操作 |
| 线程终止规则 | 线程内操作先于 join() 返回 |
面试不一定要把所有规则背得很细,但要知道:
happens-before 用来描述多线程之间的可见性关系。
八、什么是 CAS?
CAS 全称是 Compare And Swap,比较并交换。
它是一种乐观锁思想。
CAS 有三个值:
内存值 V
期望值 A
新值 B
逻辑是:
如果 V == A,就把 V 改成 B;
否则更新失败。
可以这样理解:
我以为 count 还是 5;
如果它真是 5,我就改成 6;
如果不是 5,说明别人改过了,我就失败或重试。
九、CAS 为什么是原子的?
CAS 底层依赖 CPU 原子指令。
Java 中很多 CAS 操作通过 Unsafe 或 VarHandle 实现,最终依赖硬件保证“比较并交换”这一步不可被打断。
所以 CAS 不需要像 synchronized 那样阻塞线程,属于无锁并发的一种基础。
十、CAS 有什么问题?
CAS 常见问题有三个:
| 问题 | 说明 |
|---|---|
| ABA 问题 | 值从 A 变 B 又变回 A,CAS 以为没变 |
| 自旋开销 | 失败后不断重试,竞争激烈时浪费 CPU |
| 只能保证单变量原子更新 | 多个变量一致更新比较复杂 |
自旋开销很好理解:
CAS 失败 -> 重试
又失败 -> 再重试
竞争越激烈,CPU 空转越明显
所以 CAS 适合竞争不是特别激烈、逻辑比较短的场景。
十一、什么是 ABA 问题?
CAS 只检查值有没有变成期望值。
比如线程 A 看到变量是:
A
然后线程 B 把它改成:
A -> B -> A
线程 A 再执行 CAS 时发现还是 A,就以为没变过,于是更新成功。
但实际上它中间被改过。
这就是 ABA 问题。
解决思路:
加版本号
例如:
A, version = 1
B, version = 2
A, version = 3
虽然值又回到了 A,但版本号变了,CAS 就能发现中间发生过修改。
十二、AtomicInteger 底层是什么?
AtomicInteger 底层主要基于 CAS。
常见方法:
incrementAndGet()
getAndIncrement()
compareAndSet()
示例:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
它可以保证自增操作的原子性。
比下面这种更安全:
volatile int count = 0;
count++;
十三、AtomicInteger 和 volatile int 有什么区别?
| 对比 | volatile int | AtomicInteger |
|---|---|---|
| 可见性 | 保证 | 保证 |
| 原子性 | 不保证 count++ 原子性 |
保证原子更新 |
| 底层机制 | 内存屏障 | CAS |
| 适合场景 | 状态标记 | 原子计数 |
所以计数器场景应该用:
AtomicInteger
而不是只用 volatile。
十四、LongAdder 是什么?
LongAdder 也是计数器,但它在高并发下通常比 AtomicLong 性能更好。
原因是它会把热点计数拆散到多个 Cell 上:
base + Cell1 + Cell2 + Cell3 ...
多个线程可以分散更新不同 Cell,减少 CAS 竞争。
取值时再汇总。

适合高并发统计,比如:
QPS
请求次数
接口访问量
十五、synchronized 锁升级是什么?
为了优化性能,JVM 对 synchronized 做了锁升级。
经典过程:
无锁
↓
偏向锁
↓
轻量级锁
↓
重量级锁
意思是:
根据竞争程度逐步升级,而不是一开始就使用重量级操作系统互斥量。
不过要注意,不同 JDK 版本对偏向锁支持有变化,面试掌握思想即可。
十六、轻量级锁和重量级锁有什么区别?
轻量级锁主要通过 CAS 尝试获取锁,适合竞争不激烈的场景。
重量级锁会让线程阻塞和唤醒,涉及操作系统调度,开销更大。
可以简单记:
竞争少:CAS 自旋,轻量
竞争激烈:线程阻塞,重量
所以 synchronized 不是一上来就很重,它会根据竞争情况进行优化。
十七、ReentrantLock 是什么?
ReentrantLock 是 JUC 包下的可重入锁。
示例:
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
一定要在 finally 中释放锁,防止异常导致锁不释放。
十八、ReentrantLock 和 synchronized 有什么区别?
| 对比 | synchronized | ReentrantLock |
|---|---|---|
| 类型 | JVM 内置关键字 | JUC 工具类 |
| 释放锁 | 自动释放 | 手动 unlock |
| 可中断 | 不方便 | 支持 lockInterruptibly |
| 尝试加锁 | 不支持 | 支持 tryLock |
| 公平锁 | 不支持指定 | 支持公平/非公平 |
| 条件队列 | 一个 wait set | 多个 Condition |
简单说:
synchronized 简单易用,ReentrantLock 更灵活。
十九、什么是可重入锁?
可重入锁指同一个线程拿到锁后,可以再次获得同一把锁。
例如:
synchronized void a() {
b();
}
synchronized void b() {
}
同一个线程进入 a() 后已经拿到了锁,再调用 b() 时可以继续进入,不会把自己锁死。
synchronized 和 ReentrantLock 都是可重入锁。
二十、公平锁和非公平锁有什么区别?
公平锁:
线程按排队顺序获取锁
非公平锁:
新来的线程可以尝试插队抢锁
ReentrantLock 默认是非公平锁:
new ReentrantLock(); // 非公平锁
new ReentrantLock(true); // 公平锁
非公平锁吞吐量通常更高,因为减少线程切换。
公平锁更“按顺序”,但性能可能差一些。
二十一、这一组怎么串起来讲?
可以这样回答:
JMM 是 Java 内存模型,主要解决多线程下共享变量的可见性和有序性问题。
线程操作共享变量时,可能先读到自己的工作内存,导致其他线程修改不可见。
volatile 通过内存屏障保证可见性,并禁止特定指令重排序。
CAS 是比较并交换,属于乐观锁思想,底层依赖 CPU 原子指令。
AtomicInteger 基于 CAS,能保证自增原子性,但 CAS 有 ABA、自旋开销和单变量限制。
LongAdder 通过分段计数降低高并发下的 CAS 竞争。
synchronized 有锁升级优化,不是一开始就重量级。
ReentrantLock 比 synchronized 更灵活,支持 tryLock、可中断、公平锁和多个 Condition。
总体流程图

总结
这一组可以按下面这条线来记:
JMM 解决多线程下可见性和有序性问题。
线程操作共享变量可能先读到工作内存,导致其他线程修改不可见。
volatile 通过内存屏障保证可见性,并禁止特定重排序。
happens-before 用来描述操作之间的可见性关系。
CAS 是比较并交换,属于乐观锁思想,底层依赖 CPU 原子指令。
AtomicInteger 基于 CAS,能保证自增原子性。
CAS 有 ABA、自旋开销、只能更新单变量等问题。
LongAdder 通过分段计数降低高并发竞争。
synchronized 有锁升级优化,不是一开始就重量级。
ReentrantLock 比 synchronized 更灵活,支持 tryLock、可中断、公平锁和多个 Condition。
这一组重点背:JMM、主内存/工作内存、volatile 内存屏障、happens-before、CAS、ABA、AtomicInteger、LongAdder、锁升级、ReentrantLock vs synchronized、公平锁/非公平锁。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)