2026年7月11日 · 4 分钟阅读
synchronized 与 volatile:Java 并发基础关键字原理
梳理 synchronized 的可见性、monitorenter 和 monitorexit、Monitor、锁升级,以及 volatile 的内存屏障、适用场景和原子性缺陷。
synchronized 和 volatile 是 Java 并发里最基础、也最容易误用的两个关键字。
一句话区分:synchronized 解决互斥和可见性,volatile 主要解决可见性和有序性。
synchronized 能做什么
synchronized 可以修饰方法,也可以修饰代码块。
它能保证:
- 原子性:同一时刻只有一个线程执行同步代码。
- 可见性:释放锁前刷新共享变量,加锁后重新读取共享变量。
- 有序性:锁保护范围内的执行结果不会被重排序破坏。
示例:
public synchronized void add() {
count++;
}
public void add2() {
synchronized (this) {
count++;
}
}
同步方法锁住的是当前实例对象;静态同步方法锁住的是 Class 对象。
synchronized 如何保证可见性
JMM 对锁有明确约束:
- 线程解锁前,必须把本地内存中的共享变量刷新到主内存。
- 线程加锁时,会清空本地内存中的共享变量副本,重新从主内存读取。
所以 synchronized 不只是“排队执行”,它还建立了线程之间的可见性关系。
monitorenter 和 monitorexit
同步代码块在字节码层面主要依赖两个指令:
monitorentermonitorexit
进入同步代码块时执行 monitorenter 获取 Monitor;退出同步代码块时执行 monitorexit 释放 Monitor。
编译器通常会生成额外的 monitorexit,用于异常情况下释放锁。可以理解为同步块隐含了类似 try-finally 的保护,避免异常导致锁无法释放。
什么是 Monitor
每个 Java 对象都可以作为锁,是因为对象可以和 Monitor 关联。
线程尝试进入同步代码块时,需要获取对象关联的 Monitor。如果 Monitor 已经被其他线程持有,当前线程就会阻塞。
Monitor 负责管理:
- 哪个线程持有锁。
- 锁的重入次数。
- 哪些线程在等待锁。
- 哪些线程在等待通知。
这也是为什么 wait()、notify()、notifyAll() 必须在同步代码块中使用,因为它们依赖对象 Monitor。
锁升级
早期 synchronized 被认为是重量级锁,因为它可能涉及操作系统互斥量和线程阻塞唤醒,成本较高。
JDK 1.6 之后,JVM 对 synchronized 做了大量优化,锁状态大致包括:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
不同状态适合不同竞争程度。
| 锁状态 | 适用场景 |
|---|---|
| 无锁 | 没有同步竞争 |
| 偏向锁 | 只有一个线程反复进入同步块 |
| 轻量级锁 | 多个线程交替竞争,但竞争不激烈 |
| 重量级锁 | 竞争激烈,需要阻塞和唤醒线程 |
锁升级的目标是:在低竞争场景下减少系统调用和线程阻塞。
volatile 能做什么
volatile 主要保证两件事:
- 可见性:一个线程修改后,其他线程能读到最新值。
- 有序性:禁止特定指令重排序。
示例:
private volatile boolean running = true;
public void stop() {
running = false;
}
这种停止标记是 volatile 的典型适用场景。
volatile 的实现基础:内存屏障
volatile 的底层依赖内存屏障。
内存屏障可以理解为一种约束,告诉编译器和处理器:屏障前后的某些读写操作不能随意重排序,并且要按规定刷新或读取内存。
JMM 对 volatile 的重排序限制可以简化理解为:
volatile写之前的普通写,不能被重排序到volatile写之后。volatile读之后的普通读写,不能被重排序到volatile读之前。- 对同一个
volatile变量的写和读之间建立 happens-before 关系。
volatile 的缺陷:不保证原子性
volatile 不能保护复合操作。
volatile int count = 0;
public void add() {
count++;
}
count++ 包含读取、加一、写回。volatile 可以让每次读写尽量看到最新值,但不能阻止两个线程同时读到同一个旧值。
解决方式可以是:
synchronized。ReentrantLock。AtomicInteger。
synchronized 和 volatile 对比
| 对比项 | synchronized | volatile |
|---|---|---|
| 原子性 | 支持 | 不支持复合操作原子性 |
| 可见性 | 支持 | 支持 |
| 有序性 | 支持 | 支持特定重排序限制 |
| 是否阻塞线程 | 可能阻塞 | 不阻塞 |
| 使用场景 | 临界区、复合操作、状态保护 | 状态标记、配置开关、单写多读变量 |
如果一个变量的更新依赖当前值,例如递增、扣库存、累加金额,不能只用 volatile。
使用建议
可以按这个思路选择:
- 只需要让其他线程看到最新状态,用
volatile。 - 需要保护一段临界区,用
synchronized或 Lock。 - 需要高性能数值更新,用原子类或 LongAdder。
- 需要等待、通知、可中断、超时、公平锁,用 JUC 锁。
小结
synchronized 和 volatile 都和 JMM 有关,但解决的问题不同。
synchronized 是完整的同步机制,既管互斥,也管可见性;volatile 是轻量级可见性工具,适合状态标记,但不适合复合更新。
真正写并发代码时,先判断问题是原子性、可见性还是有序性,再选择工具。