synchronized 的原理是什么?
未标记
考查重点:对象监视器与同步可见性。获取对象 monitor 后进同步区,退出释放并建立可见性,同线程可重入
本题目录
参考答案
核心回答
synchronized 通过对象的监视器,也就是 monitor,实现互斥与线程间可见性。线程先获得指定对象的监视器,再执行同步区域;其他线程要获取同一监视器,需等待它可用。同一线程可以重复进入,所以它是可重入的。
它在字节码中有两种主要表达:同步代码块使用 monitorenter、monitorexit;同步方法使用 ACC_SYNCHRONIZED 标志,由 JVM 在方法调用和退出时完成监视器的进入与释放。正常返回和异常退出都要正确释放持有的同步层次。
内存语义上,对同一监视器的解锁 happens-before 后续加锁,因而按同一锁协议访问共享状态的线程可以看到之前的相关修改。具体如何采用轻量锁、竞争等待等优化,是 JVM 实现和版本的事情,不能把旧 HotSpot 的固定“锁升级四阶段”当成 Java 语义。
先确定锁的是哪个对象
- 实例同步方法锁的是接收调用的
this对象。 - 静态同步方法锁的是定义该方法的类所对应的
Class对象。 - 同步代码块锁的是括号中表达式得到的对象。
例如,同一个库存对象的“检查并扣减”和“读取剩余库存”都采用这把锁,才能形成完整的保护;若各自创建一个新锁对象,就无法互斥。两个不同实例上的实例同步方法也不因方法名相同而互斥,实例锁与 Class 锁通常同样是不同的锁。
锁不会让对象的全部访问自动变安全。其他代码绕过这把锁直接修改库存,或使用另一把锁,仍会破坏保护条件。
可重入与异常退出怎样工作
可以从语义上理解为记录持有者和重入层次:首次获得锁进入一层;同一线程再次进入同一对象的同步区域,再增加一层;每退出一层匹配一次释放,全部退出后其他线程才可获得。
因此,一个实例同步方法调用同一对象的另一个实例同步方法,不会因为自己已经持锁而阻塞。但线程先持有 A 再等 B,另一个线程先持有 B 再等 A,仍可能死锁,可重入并不消除多把锁之间的循环等待。
异常离开同步代码块时,编译器生成的异常处理路径保证释放;同步方法由 JVM 按同步方法的调用约定退出监视器。释放锁不代表业务修改回滚,如果异常前已经把共享对象改到不完整状态,仍需在业务逻辑中维护不变量。
为什么不只是“同一时刻一个线程执行”
互斥保护的是整个临界区里的操作,例如把检查库存和减少库存放在同一保护范围,避免两个线程都基于旧值行动。可见性则保证前一持有者释放锁前的相关写入,对后续获得同一锁的线程可见。
这允许 JVM 和处理器在保持规定语义的前提下优化执行,不意味着同步区域内的每条指令都禁止重排,也不必解释成“每次把整个 CPU 缓存刷回内存”。读取方若完全绕过同步,不能仅凭写入方使用 synchronized 就推导出同样保证。
追问:wait、notify 和锁优化有什么关系
wait、notify、notifyAll 都与目标对象的监视器和等待集相关,需要当前线程已经持有该监视器。wait 等待时释放目标对象的监视器,恢复执行前重新获取;notify 只是使等待线程有机会恢复,不会立即交出锁,醒来后还应循环检查条件。sleep 则不会因此释放已经持有的监视器。
在实现层面,低竞争时可以用较短的获取路径,竞争激烈时需要更完整的监视器与等待管理;JIT 还可能在满足条件时进行锁消除、锁粗化等优化。历史资料中的偏向锁和栈锁记录要限定版本:偏向锁自 JDK 15 起默认关闭,不能照搬为 Java 25 的常规执行阶段,也不能断言所有锁都只会单向升级、永远无法撤销膨胀。
参考资料
- 国内:JavaGuide:Java 并发常见面试题(中):参考实例锁、Class 锁、代码块指令与同步方法标志的比较,解释正文中不同 synchronized 写法如何对应 monitor 同步。
- 国内:JavaGuide:Java 内存模型:用于解释互斥之外的可见性要求,以及没有统一同步协议时为何仍存在数据竞争。
- 国内:美团技术团队:不可不说的 Java“锁”事:用于说明同一线程重入同一把锁,以及竞争下自旋和阻塞的不同成本;旧版对象头与偏向锁流程需结合 JDK 版本理解。
- 国外:Java 语言规范第 17 章:Threads and Locks:用于核对监视器互斥、重入、正常与异常退出、wait/notify 以及同一监视器的 happens-before 关系。
- 国外:Java 虚拟机规范:Synchronization,Java SE 25:用于核对 ACC_SYNCHRONIZED、monitorenter 和 monitorexit 的分工,区分规定的同步语义与 JVM 内部优化。

