2026年7月11日 · 4 分钟阅读

synchronized 与 volatile:Java 并发基础关键字原理

梳理 synchronized 的可见性、monitorenter 和 monitorexit、Monitor、锁升级,以及 volatile 的内存屏障、适用场景和原子性缺陷。

synchronizedvolatile 是 Java 并发里最基础、也最容易误用的两个关键字。

一句话区分:synchronized 解决互斥和可见性,volatile 主要解决可见性和有序性。

synchronized 能做什么

synchronized 可以修饰方法,也可以修饰代码块。

它能保证:

  • 原子性:同一时刻只有一个线程执行同步代码。
  • 可见性:释放锁前刷新共享变量,加锁后重新读取共享变量。
  • 有序性:锁保护范围内的执行结果不会被重排序破坏。

示例:

public synchronized void add() {
    count++;
}

public void add2() {
    synchronized (this) {
        count++;
    }
}

同步方法锁住的是当前实例对象;静态同步方法锁住的是 Class 对象。

synchronized 如何保证可见性

JMM 对锁有明确约束:

  1. 线程解锁前,必须把本地内存中的共享变量刷新到主内存。
  2. 线程加锁时,会清空本地内存中的共享变量副本,重新从主内存读取。

所以 synchronized 不只是“排队执行”,它还建立了线程之间的可见性关系。

monitorenter 和 monitorexit

同步代码块在字节码层面主要依赖两个指令:

  • monitorenter
  • monitorexit

进入同步代码块时执行 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 对比

对比项synchronizedvolatile
原子性支持不支持复合操作原子性
可见性支持支持
有序性支持支持特定重排序限制
是否阻塞线程可能阻塞不阻塞
使用场景临界区、复合操作、状态保护状态标记、配置开关、单写多读变量

如果一个变量的更新依赖当前值,例如递增、扣库存、累加金额,不能只用 volatile

使用建议

可以按这个思路选择:

  • 只需要让其他线程看到最新状态,用 volatile
  • 需要保护一段临界区,用 synchronized 或 Lock。
  • 需要高性能数值更新,用原子类或 LongAdder。
  • 需要等待、通知、可中断、超时、公平锁,用 JUC 锁。

小结

synchronizedvolatile 都和 JMM 有关,但解决的问题不同。

synchronized 是完整的同步机制,既管互斥,也管可见性;volatile 是轻量级可见性工具,适合状态标记,但不适合复合更新。

真正写并发代码时,先判断问题是原子性、可见性还是有序性,再选择工具。