真实面经题目 · 原创解析
synchronized锁底层CS了解么?
synchronized 是 JVM 提供的内置互斥机制,核心不是简单的操作系统锁,而是围绕对象头 Mark Word、Monitor 监视器和字节码 monitorenter/monitorexit 协作完成。它支持可重入,具备明确的内存可见性语义,并会结合轻量级锁、CAS、重量级锁以及锁消除、锁粗化等优化来降低同步成本。
真实面经题目 · 原创解析
synchronized 是 JVM 提供的内置互斥机制,核心不是简单的操作系统锁,而是围绕对象头 Mark Word、Monitor 监视器和字节码 monitorenter/monitorexit 协作完成。它支持可重入,具备明确的内存可见性语义,并会结合轻量级锁、CAS、重量级锁以及锁消除、锁粗化等优化来降低同步成本。
可以从 JVM 层面回答:synchronized 修饰代码块时,编译后会在进入同步块前后生成 monitorenter 和 monitorexit;修饰实例方法或静态方法时,会通过方法访问标志表达同步语义,实际仍然是对对象或 Class 对象关联的 Monitor 加锁。对象头里的 Mark Word 会记录锁状态、线程相关信息、hashCode、GC 年龄等内容。早期 JVM 为了降低无竞争场景下的成本,引入过偏向锁;更通用的理解是锁会根据竞争情况经历无锁、轻量级锁、重量级锁等状态变化。轻量级锁主要依赖 CAS 尝试把对象头的 Mark Word 指向当前线程栈帧中的锁记录,竞争激烈时会膨胀为重量级 Monitor,线程可能进入阻塞和唤醒流程。synchronized 还是可重入锁,同一线程重复进入同一把锁不会死锁,Monitor 会记录重入次数。同时,它还提供内存语义:进入同步块相当于 acquire,退出同步块相当于 release,保证临界区内对共享变量的修改对后续获得同一把锁的线程可见。JIT 还会做锁消除和锁粗化,分别针对逃逸分析确认无共享的锁、以及连续细碎加锁的场景进行优化。
synchronized 加锁的对象本身会参与锁状态表达,关键入口是对象头中的 Mark Word。Mark Word 是一块复用的运行时元数据区域,会在不同状态下存放 hashCode、GC 年龄、锁标志位、线程相关指针或 Monitor 指针等信息。理解 synchronized 时不要把锁想成独立变量,而要知道对象头会随着锁竞争情况发生状态切换。轻量级锁时,它通常和线程栈帧里的锁记录建立关联;膨胀后,则会关联到重量级 Monitor。
Monitor 可以理解为 JVM 为 synchronized 抽象出的监视器锁结构,负责互斥进入、重入计数、等待队列、阻塞线程唤醒等逻辑。每个对象都可以在需要时关联一个 Monitor,但并不是每个对象一创建就分配重量级 Monitor。竞争不激烈时,JVM 会尽量用对象头和线程栈上的锁记录完成低成本同步;当 CAS 失败、竞争明显或需要阻塞等待时,锁会膨胀,Monitor 才成为主要协调者。
同步代码块在字节码层面会使用 monitorenter 和 monitorexit。线程执行 monitorenter 时尝试获取目标对象的监视器,成功后才能进入临界区;执行 monitorexit 时释放监视器,并让其他等待线程有机会继续竞争。编译器还会保证异常路径也能执行释放逻辑,所以同步块通常会生成多个 monitorexit,以避免异常导致锁无法释放。同步方法的表达方式不同,但语义上仍然是进入方法时获取锁、退出方法时释放锁。
synchronized 的性能优化重点在于根据竞争情况选择合适锁形态。常见理解路径是无锁、偏向锁背景、轻量级锁、重量级锁。无竞争时不需要真正阻塞线程;轻量级锁通过 CAS 修改对象头,失败后可能自旋或进入膨胀流程;当竞争持续存在,JVM 会把锁升级为重量级 Monitor,线程进入阻塞等待。偏向锁属于历史优化背景,适合理解早期 JVM 如何减少同一线程反复加锁的成本,但回答时不必绑定具体版本细节。
轻量级锁并不是完全不加锁,而是把互斥控制尽量放在用户态和对象头更新上。线程进入同步块时,会在自己的栈帧中创建锁记录,并尝试用 CAS 把对象头 Mark Word 替换为指向该锁记录的指针。如果 CAS 成功,说明没有其他线程持有该锁;如果失败,说明可能存在竞争、重入或状态变化。JVM 会进一步判断是当前线程重入,还是需要自旋、膨胀到重量级锁。
synchronized 是可重入的,同一线程已经持有某个对象锁时,可以再次进入被同一把锁保护的同步代码,而不会把自己阻塞住。实现上,轻量级锁和重量级 Monitor 都需要识别锁持有者是否为当前线程;如果是重入,就增加对应的重入记录或计数。退出时也不是第一次 monitorexit 就完全释放,而是要等重入层数全部退出后,锁才真正对其他线程可用。
synchronized 不只解决互斥,还解决可见性和有序性。线程释放锁时,会把临界区内对共享变量的修改刷新到主内存语义上可见的位置;另一个线程随后获得同一把锁时,能够看到之前释放锁线程的修改结果。这对应 happens-before 规则中的监视器锁规则:对一个锁的解锁先行发生于后续对同一个锁的加锁。因此 synchronized 可以替代一部分 volatile 和显式内存屏障场景。
现代 JVM 会尽量避免无意义的 synchronized 成本。锁消除依赖逃逸分析,如果确认被加锁对象不会被其他线程访问,那么同步操作可以被去掉。锁粗化则针对连续、频繁、粒度过细的加锁解锁,把多个相邻同步区域合并为更大的同步范围,减少反复进入和退出锁的开销。这些优化说明 synchronized 的真实性能不能只按最坏的重量级锁理解。
修饰普通实例方法时,锁的是当前实例对象,也就是 this。修饰静态方法时,锁的是该类对应的 Class 对象。两者不是同一把锁,所以一个线程进入实例同步方法,不会天然阻塞另一个线程进入该类的静态同步方法,除非它们显式使用了同一个锁对象。
synchronized 是 JVM 内置锁,语法简单,异常退出时自动释放,并且能被 JIT 深度优化。ReentrantLock 是显式锁,需要手动 unlock,但提供可中断加锁、超时尝试、公平锁、多个 Condition 等能力。一般简单互斥优先 synchronized,需要更强控制能力时再考虑 ReentrantLock。
因为 Java 内存模型规定,对同一个监视器锁的解锁 happens-before 后续对该锁的加锁。线程退出 synchronized 时,临界区内的写入会对之后获得同一把锁的线程可见;进入 synchronized 时,也会按锁语义读取到前一个持锁线程释放前的结果。
轻量级锁希望在没有真实竞争时避免线程阻塞和内核态切换。线程会通过 CAS 尝试把对象头 Mark Word 更新为指向自己栈帧锁记录的指针。CAS 成功说明获取锁成本很低;CAS 失败则说明可能存在竞争、重入或锁状态变化,需要进一步处理。
锁消除解决的是无必要同步,例如对象经过逃逸分析确认只在当前线程内使用,那么加锁没有并发意义,可以去掉。锁粗化解决的是频繁短同步带来的进入退出成本,把连续的小同步范围合并,减少重复加锁解锁。