深入剖析 synchronized 案例:从锁升级到死锁预防,一篇讲透并发安全
目录导读
- 引言:一个崩溃的订单系统引发的思考
- synchronized 核心机制回顾(附案例)
- 静态方法与实例方法的锁差异
- 锁升级与偏向锁撤销的实战观察
- 可重入性与死锁的经典双线程案例
- synchronized + wait/notify 生产者消费者
- 高频问答:面试官最爱问的 5 个问题
- 总结与避坑指南
一个崩溃的订单系统引发的思考
假设你负责一个电商系统的库存扣减模块,上线第一天就出现超卖:100 件商品卖出 120 单,排查后定位到核心代码:

public void deductStock(int count) {
int stock = getStock(); // 从数据库读取
if (stock >= count) {
stock = stock - count;
updateStock(stock); // 写回数据库
}
}
此时如果两个线程同时读到 stock=10,都判断 10>=5,然后都写回 5,库存就变成 5 而非 0——典型的竞态条件,而 Java 中最基础、最直接的解决方案就是 synchronized,本文通过 4 个完整案例,带你彻底掌握它的用法、原理与陷阱。
synchronized 核心机制回顾(附案例)
synchronized 是 Java 内置的互斥锁,保证同一时刻只有一个线程执行临界区代码,它锁定的是对象头(Object Header)中的 Mark Word,而非代码本身,基本语法有两种:
// 同步方法
public synchronized void methodA() { ... }
// 同步代码块
public void methodB() {
synchronized(this) { ... }
}
关键点:锁的对象是谁,决定了线程阻塞的粒度,锁
this只锁当前实例;锁Class对象则锁所有实例。
案例一:静态方法与实例方法的锁差异
场景
一个工具类有静态方法 syncStatic() 和实例方法 syncInstance(),两个线程分别调用它们。
public class LockDiff {
public synchronized static void syncStatic() {
System.out.println("静态方法开始");
sleep(2000);
System.out.println("静态方法结束");
}
public synchronized void syncInstance() {
System.out.println("实例方法开始");
sleep(2000);
System.out.println("实例方法结束");
}
}
测试结果
两个线程同时执行,互不阻塞,几乎同时完成。
- 静态方法锁的是
LockDiff.class(类锁) - 实例方法锁的是
this(对象锁) - 两者是不同的锁,所以不互斥
踩坑提示
如果静态方法内要访问共享静态变量,建议统一使用类锁,否则可能产生不一致。
案例二:锁升级与偏向锁撤销的实战观察
场景
JDK 6 之后的 synchronized 是重量级锁的“升级版”,经历 无锁→偏向锁→轻量级锁→重量级锁 的过程,我们用一段代码观察锁对象头的变化(通过 JOL 工具):
public class LockUpgrade {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
Thread.sleep(4000); // 等待偏向延迟
synchronized (obj) {
System.out.println("偏向锁阶段:");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
}
输出观察
- 无锁状态:Mark Word 末尾两位为 01
- 偏向锁:末尾 101,且记录了线程 ID
- 当另一个线程竞争时,偏向锁撤销,升级为轻量级锁(00),再竞争激烈时升级为重量级锁(10)
实际意义
- 偏向锁适合单线程访问的场景,撤销时需要 STW(安全点)
- 重量级锁依赖 OS 互斥量,会挂起线程,性能最差
- 所以不要过度使用 synchronized 包装无关紧要的代码,但如果锁竞争不激烈,性能是可接受的
案例三:可重入性与死锁的经典双线程案例
可重入性证明
同一个线程可以重复获得同一把锁:
public synchronized void outer() {
System.out.println("外层");
inner(); // 同一个线程,允许进入
}
public synchronized void inner() {
System.out.println("内层");
}
死锁案例
线程 A 持有锁1 等待锁2;线程 B 持有锁2 等待锁1。
final Object lockA = new Object();
final Object lockB = new Object();
// 线程A
new Thread(() -> {
synchronized (lockA) {
sleep(100);
synchronized (lockB) { ... }
}
}).start();
// 线程B
new Thread(() -> {
synchronized (lockB) {
sleep(100);
synchronized (lockA) { ... }
}
}).start();
实际检测
用 jstack <pid> 可以看到“Found one Java-level deadlock”,并显示线程互相等待的锁。
预防策略
- 加锁顺序一致(都先锁 lockA 再锁 lockB)
- 使用
tryLock超时返回(但 synchronized 不支持,需换 ReentrantLock) - 减少锁的持有时间,避免嵌套锁
案例四:synchronized + wait/notify 生产者消费者
场景
一个容量为 1 的缓冲区,生产者放数据,消费者取数据。
class Buffer {
private final Object lock = new Object();
private int data;
public void put(int value) throws InterruptedException {
synchronized (lock) {
while (data != 0) { // 用 while 而非 if,防止虚假唤醒
lock.wait();
}
data = value;
lock.notifyAll();
}
}
public int get() throws InterruptedException {
synchronized (lock) {
while (data == 0) {
lock.wait();
}
int v = data;
data = 0;
lock.notifyAll();
return v;
}
}
}
要点
wait()会释放锁并进入等待队列notify()随机唤醒一个,notifyAll()唤醒所有,推荐后者- 必须在 synchronized 块内调用,否则抛 IllegalMonitorStateException
易错点
while 循环判断条件,而不是 if,因为线程被唤醒后可能竞争不到锁,或者条件又被改变,需重新检查。
高频问答:面试官最爱问的 5 个问题
Q1:synchronized 和 ReentrantLock 有什么区别? A:两者都是可重入,synchronized 自动释放锁,ReentrantLock 需手动 unlock(配合 try-finally),synchronized 支持锁升级,ReentrantLock 支持公平锁、中断、超时、多条件(Condition),性能上无大差异,现代 JVM 中 synchronized 已优化。
Q2:锁的是代码还是对象? A:锁的是对象,无论是同步方法还是同步块,最终都要指定一个锁对象,锁对象可以是实例、Class 对象,或者任意 Object。
Q3:为什么 wait/notify 必须放在 synchronized 里? A:因为要确保 wait 操作前已经拿到锁,否则无法正确管理等待队列,也避免“丢失唤醒”问题(Lost Wakeup),wait 释放锁必须基于锁对象,需要对象头中的 monitor 记录。
Q4:synchronized 能否保证可见性? A:可以,进入 synchronized 块(获取锁)会清空工作内存,同步块结束(释放锁)会把修改刷新到主内存,这是 Java 内存模型(JMM)中 Happens-Before 规则保证的。
Q5:实战中如何选择锁? A:如果资源竞争不激烈,优先 synchronized,简单可靠,如果涉及公平性、超时、可中断、多条件队列,改用 ReentrantLock,若追求极致性能,可以考虑无锁(CAS)、LongAdder、ReadWriteLock 等。
总结与避坑指南
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 单实例方法 | synchronized 方法 | 简单直观 |
| 静态变量 | synchronized(Class) | 全局锁 |
| 多条件协作 | wait/notify | 内置支持 |
| 有超时需求 | ReentrantLock | 灵活可控 |
避坑清单:
- 锁的对象不要用
String常量或基本类型包装类,因为常量池会复用导致锁冲突。 - 不要在同步块内调用
Thread.sleep()或网络 IO,会拖长时间。 - 不要将
synchronized用在 Spring Bean 的代理方法上,注意 AOP 是否会改变锁对象。 - 多线程操作集合时,用
Collections.synchronizedList或ConcurrentHashMap,但慎用synchronized(list)遍历。
最后一句:synchronized 不是银弹,但它是并发编程的地基,通过上述案例,希望你能真正理解“锁的是对象,护的是数据,防的是竞态”,写代码时多想想锁的粒度、顺序和释放,就能避开大部分并发陷阱。