synchronized到底锁的是什么:对象头 Mark Word、Monitor、锁升级与排查

你在面试里被问 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 抓锁持有者与等待者
相关推荐
ywlovecjy2 小时前
Tomcat下载,安装,配置终极版(2024)
java·tomcat
二进制person2 小时前
JavaEE初阶 --JVM
java·java-ee
北风toto2 小时前
IDEA模块名字和文件夹名字不一样的解决方式
java·ide·intellij-idea
程途知微2 小时前
synchronized锁升级全流程解析
java
亓才孓2 小时前
[Java笔试]易错点总结
java·开发语言
SimonKing2 小时前
企微、QQ统统接入OpenClaw,蓄水池已满,准备养虾
java·后端·程序员
:1212 小时前
java---过滤器,监听器
java·开发语言
洛阳泰山2 小时前
我用 Java 21 虚拟线程重写了一个 RAG 平台:从架构设计到踩坑实录
java·人工智能·后端
永远睡不够的入2 小时前
C++继承详解
java·c++·redis