前几周做代码评审,看到同事写了一个 synchronized 方法,又有一个新同事问:“这个锁到底锁的是方法,还是锁的是对象?” 当时很多人随口就答“锁的是方法”,但真让他解释对象头、Monitor、锁升级,场面就安静了。我自己也是在一次线上超卖事故里被迫把 synchronized 的底层链路啃了一遍,越往深处越发现,JDK 对这把锁做的优化远比想象中多。这篇文章不打算背八股,就从那段出问题的代码讲起,沿着“用法 → 字节码 → 对象头 → Monitor → JDK 优化 → 实测踩坑”这条线,把 synchronized 从表面到原理完整捋一遍。
1. 从一个并发事故讲起:synchronized 锁的到底是什么
1.1 一段能稳定复现的并发 Bug
先看一段非常典型的代码。两个线程同时执行 increment(),理想结果当然是 200000,但实际跑起来经常是 19 万出头,甚至更少。
java复制public class SyncDemo {
private int count = 0;
public void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
SyncDemo demo = new SyncDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
demo.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
demo.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(demo.count);
}
}
很多人第一反应是“给 count++ 加锁”。这个方向没错,但必须说清楚为什么。count++ 看起来是一行代码,编译成字节码之后其实至少三步:读取当前值、加 1、写回。两个线程可能同时读到同一个旧值,各自加 1 后再写回,结果就少了一次递增。这就是典型的“读改写”竞态条件。
如果把 increment() 声明为 synchronized,结果会稳定变成 200000。所以 synchronized 做的最核心的一件事,就是保证同一时刻只有一个线程能进入受保护的临界区,并且让临界区内的读写对其他线程可见。
1.2 锁的职责不是“阻止执行”,而是“划定临界区”
我见过不少刚入行的同学会把“加锁”理解成“让代码不能执行”,这是错的。锁的真正作用是划定临界区,并规定进入临界区的规则:同一时刻只允许一个线程持有锁;其他线程想进来,必须等在门外。等前一个线程退出临界区并释放锁,等待者才有机会竞争。
用生活里的例子类比,厕所门上的插销就是一把锁。门本身不阻止任何人靠近,但插销一旦扣上,同一时间只有里面的人能用。外面的人要么等,要么换地方。代码里的临界区就是“厕所”,锁对象就是“插销”。
这里有个很容易搞混的点:synchronized 修饰代码块时,括号里的对象决定用哪把锁;修饰方法时,JVM 默认给你指定了锁。也就是说,synchronized 从来都不是在锁“代码”,而是在锁“对象”,代码只是被这个对象锁保护起来的区域。
1.3 每个 Java 对象都自带一把“内置锁”
这是理解 synchronized 的基石:在 HotSpot 虚拟机里,任何一个 Java 对象天生就携带一把可以被 synchronized 使用的锁,官方术语叫内置锁,也叫 Monitor 锁。无论是 new Object() 还是自定义的实体类对象,它都有这把锁,只是平时没人用它而已。
这把锁和对象本身强绑定,存在对象头里。于是“给方法加锁”“给代码块加锁”在底层全都等价于“拿某个对象的 Monitor”。于是问题就来了:到底拿哪个对象的?这就引出了 synchronized 的三种经典用法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三种加锁姿势与方法级/块级锁的等价关系
2.1 实例方法、静态方法、同步代码块到底锁谁
jdk 中 synchronized 的三种写法,区别只在锁对象的选择上。下面这张表建议直接收藏。
| 写法 | 锁对象 | 等价写法 |
|---|---|---|
| 修饰实例方法 | 当前实例 this |
synchronized (this) { ... } 包住整个方法体 |
| 修饰静态方法 | 当前类的 Class 对象 |
synchronized (Xxx.class) { ... } 包住整个方法体 |
| 修饰代码块 | 括号里手动指定的任意对象 | 无等价写法,因为锁对象可自定义 |
特别注意:synchronized 修饰静态方法和修饰实例方法,用的是两把完全不同的锁。前者是 Class 对象,后者是实例对象。如果一个类里既有静态同步方法,又有实例同步方法,两个线程可以分别进入它们,因为它们争夺的不是同一把锁。
java复制public class LockDemo {
// 锁的是 LockDemo.class
