java 的 synchronized 关键字通过内置对象监视器(monitor)实现可重入锁,其核心机制是在每个对象头中维护一个计数器。当线程获取锁时,计数器加一;进入嵌套同步块时,若持有该锁的线程再次尝试获取,则计数器继续递增,而不会发生死锁。退出同步块时,计数器递减至零,锁才真正释放。

对象头中的锁记录结构是怎样的
在 HotSpot JVM 中,每个对象实例数据分为两部分:对象头(Header)和方法区数据。对象头中包含 Mark Word,用于存储对象的哈希码、分代年龄以及锁状态标志。对于被 synchronized 修饰的代码块,JVM 会使用 Monitor 对象来实现互斥访问。Mark Word 中的锁状态指针会指向堆中的 Monitor 对象,该对象包含 owner 字段记录当前持有锁的线程 ID,以及 recursion 计数器记录重入次数。

public class ReentrantExample {
private final Object lock = new Object();
public void outer() {
synchronized (lock) {
System.out.println("Outer lock acquired, recursion: 1");
inner();
}
}
public void inner() {
synchronized (lock) {
System.out.println("Inner lock acquired, recursion: 2");
}
}
}
上述代码展示了典型的嵌套同步场景。当线程调用 outer() 时,它首先获取 lock 对象的监视器,此时 recursion 变为 1。接着调用 inner(),由于当前线程已经拥有该监视器,JVM 检测到线程 ID 匹配,直接增加 recursion 至 2 并允许执行。只有当 inner() 和 outer() 都执行完对应的 monitorenter 和 monitorexit 指令后,计数器归零,其他线程才能竞争该锁。
立即学习“Java免费学习笔记(深入)”;
你是 TypeScript 项目架构专家,专注于脚手架搭建生产就绪的 Node.js 和前端应用。生成包含现代工具链(pnpm、Vite、Next.js)、类型
字节码层面的入口与出口如何配合
从字节码角度看,synchronized 的实现依赖于 monitorenter 和 monitorexit 两条指令。编译器会在同步块的开始处插入 monitorenter,在结束处插入 monitorexit。为了确保异常情况下锁也能正确释放,编译器通常会生成两个 monitorexit,一个用于正常流程,另一个用于异常处理路径。这两个指令都会操作同一个 Monitor 对象,从而保证计数器的增减逻辑一致。

public class BytecodeExample {
public void test() {
synchronized (this) {
int x = 10;
}
}
}
编译后的字节码大致如下:aload_0 dup monitorenter iconst_10 istore_1 aload_0 monitorexit goto L1 astore_2 aload_0 monitorexit athrow L1:
这里可以看到,无论是否发生异常,monitorexit 都会被执行。每次 monitorenter 成功,Monitor 的递归计数加一;每次 monitorexit 执行,计数减一。这种设计确保了可重入性在底层指令层面得到严格保障,同时也避免了因异常导致的锁泄漏问题。
如果递归深度超过整数最大值会发生什么
虽然理论上递归深度受限于栈空间,但 synchronized 内部使用的 recursion 计数器是整型(int)。如果递归深度超过 Integer.MAX_VALUE,计数器会发生溢出,导致逻辑错误。在实际开发中,这通常意味着代码存在严重的逻辑缺陷或无限递归。JVM 并不会主动拦截这种溢出,而是让程序继续运行,可能导致锁状态混乱或意外释放。因此,应避免深层递归使用 synchronized,或在必要时改用 ReentrantLock 等更灵活的锁机制。

















