你在面试里被问 synchronized,真正的分水岭不是会不会背"它是互斥锁",而是能不能把这件事讲清楚:
synchronized锁的到底是什么资源?synchronized用在不同位置(实例方法 / 静态方法 / 代码块)锁对象分别是谁?- JVM 里它怎么实现(对象头、Monitor)?
- 为什么会有偏向锁/轻量锁/重量锁?锁升级怎么触发?
- 线上卡顿、线程阻塞、死锁怎么定位?
这篇按"从能解释 -> 能写对 -> 能排查"的主线讲透。
1. 一句话结论:synchronized 锁的是"对象",不是"代码"
synchronized 的互斥,本质是:
- 对某个锁对象(lock object)的 Monitor 进行进入/退出控制
所以你要先回答:
- 这个
synchronized对应的 锁对象到底是谁?
2. 三种写法分别锁谁(最核心、也是最常考)
2.1 修饰实例方法:锁的是"当前实例 this"
java
public class Counter {
private int count = 0;
public synchronized void inc() {
count++;
}
}
等价于:
java
public void inc() {
synchronized (this) {
count++;
}
}
结论:
- 同一个对象实例 上的
inc()会互斥 - 不同对象实例互不影响
2.2 修饰静态方法:锁的是"类对象 Counter.class"
java
public class Counter {
private static int total = 0;
public static synchronized void inc() {
total++;
}
}
等价于:
java
public static void inc() {
synchronized (Counter.class) {
total++;
}
}
结论:
- 同一个类的所有线程都竞争同一把锁
- 即使 new 多个实例,静态同步仍然互斥
2.3 同步代码块:锁的是你括号里传入的对象
java
private final Object lock = new Object();
public void inc() {
synchronized (lock) {
// critical section
}
}
结论:
- 锁对象是谁,互斥范围就围绕谁
- 锁对象必须是"共享且稳定的引用",否则锁等于没加
3. 写对的关键:锁对象必须"共享 + 不变"
3.1 典型错误:锁了一个会变的对象
java
Integer lock = 1;
public void foo() {
synchronized (lock) {
lock++; // lock 引用变了
}
}
问题:
- 你以为锁住了
lock,但下一次进入foo()锁的可能是另一个对象
正确做法:
- 使用
final Object lock = new Object()
3.2 典型错误:锁了 String 常量
java
synchronized ("LOCK") {
// ...
}
问题:
- 字符串常量池会导致不同代码路径意外共享同一把锁
正确做法:
- 使用私有的
final Object lock
4. JVM 里它怎么实现:对象头、Mark Word 与 Monitor
要讲得"像懂的人",你至少要能把这些关系说顺:
- 每个对象都有对象头(Object Header)
- 对象头里有 Mark Word(记录哈希、GC 信息、锁状态等)
- 发生同步时,JVM 会基于对象头的锁状态,把对象与 Monitor(监视器) 关联起来
你不需要把每一位比特背出来,但需要知道:
- synchronized 的锁状态会体现在对象头(Mark Word)里
- 竞争加剧时会膨胀到 Monitor,线程会在 Monitor 上阻塞/唤醒
5. monitorenter/monitorexit:编译器帮你插入的指令
synchronized 在字节码层面是:
- 进入临界区:
monitorenter - 退出临界区:
monitorexit
这也是为什么你经常听到:
- "异常也会释放锁"
原因是:
- 编译器会生成异常路径的
monitorexit,保证退出(前提是线程没有被强杀,正常异常退出会释放)
6. 可重入性:为什么同一线程可以反复进同一把锁
synchronized 是可重入的(Reentrant):
- 同一线程持有锁后,再次进入同一锁对象的同步块,不会死锁
你可以这样解释:
- Monitor 会记录"持有者线程 + 重入次数",同线程进入只递增计数
7. 锁升级:偏向锁 / 轻量锁 / 重量锁(工程上怎么理解)
面试里不用讲到源码,但建议你把"为什么要升级"讲清楚:
- 无竞争:希望加锁几乎没有成本(偏向/轻量)
- 有竞争:必须保证互斥与公平的唤醒阻塞(重量)
你可以用"成本模型"来描述:
- 偏向锁:几乎不做 CAS 竞争(偏向某线程)
- 轻量锁:少量线程竞争,用 CAS + 自旋
- 重量锁:竞争激烈,自旋浪费 CPU,转为阻塞/唤醒
工程上你看到的现象通常是:
- 自旋过多 -> CPU 飙高
- 阻塞过多 -> 上下文切换多、RT 抖动
8. wait/notify 必考点:它们依赖 Monitor,且只能在同步块内调用
规则:
wait():释放锁并进入等待队列notify()/notifyAll():唤醒等待线程(但被唤醒线程需要重新竞争锁)
常见坑:
- 在没有持有该对象 Monitor 的情况下调用
wait/notify会抛IllegalMonitorStateException
9. 常见坑与最佳实践(非常实用)
- 锁粒度尽量小:只包住共享变量的临界区,不要把 IO、RPC、长循环包进去
- 避免锁顺序不一致:容易形成死锁
- 避免在
synchronized内调用外部系统:不可控延迟会扩大锁占用时间 - 优先使用私有
final Object lock:锁对象可控,不被外部拿到
10. 线上怎么排查"谁在抢锁/谁持有锁"
10.1 先看线程状态
BLOCKED:在等待进入 Monitor(抢锁)WAITING/TIMED_WAITING:可能在wait()/park()/ sleep
10.2 用 jstack 看"锁在谁手里"
你在 jstack 里重点看两类信息:
- waiting to lock <0x...> (a xxx):谁在等锁- locked <0x...> (a xxx):谁持有锁
如果你看到多条线程围绕同一个 <0x...>,基本就是热点锁。
10.3 用 Arthas 快速定位(如果你线上允许)
thread -b:看阻塞线程jad/trace:定位热点方法(按需)
11. 面试表达(30 秒讲清楚)
synchronized锁的是对象的 Monitor。- 修饰实例方法锁
this,修饰静态方法锁Class对象,同步代码块锁括号里的对象。 - JVM 通过对象头 Mark Word 记录锁状态,竞争加剧会膨胀到 Monitor,线程在 Monitor 上阻塞/唤醒。
- 它是可重入的;
wait/notify必须在持有同一对象锁的同步块内调用。 - 排查锁竞争:看线程
BLOCKED,用jstack找谁locked/谁waiting to lock。
12. 总结
- synchronized 的关键不是"写上关键字",而是锁对象要选对
- 想讲得高级:讲清楚对象头/Monitor/锁升级的成本模型
- 想排查得快:用线程状态 +
jstack抓锁持有者与等待者