准备面试时,深入掌握一两个核心知识点,往往能成为你的‘杀手锏’。在Java并发领域,synchronized关键字的锁升级机制就是一个绝佳的选择。作为基础内容,大家对它都有了解,但深度却参差不齐。对锁升级流程的剖析,能清晰展现你对底层原理的掌握程度。
在 JDK1.6 之前,synchronized直接使用重量级锁,性能开销较大。从 JDK1.6 开始,引入了偏向锁和轻量级锁等优化。同步锁的状态从此分为四种:无锁、偏向锁、轻量级锁、重量级锁。它们会随着竞争激烈程度逐渐升级。
值得注意的是,这些锁状态信息都记录在对象的 Mark Word 中。因此,理解锁升级,必须先理解 Mark Word。
对象头 Mark Word
在 64 位 JVM 中,Mark Word 的长度为 64 位(8字节)。
Mark Word 究竟位于对象的哪个部分呢?
在 Java 堆中,每个对象的内存布局分为三部分:对象头、实例数据、对齐填充。对象头存储着对象的元数据信息,Mark Word 正是对象头的重要组成部分。它的核心作用是标识对象的线程锁状态,并可以存放对象的 hashCode。
下图展示了 Mark Word 在不同锁状态下的位分配。
Mark Word 的位分配并非一成不变,它会根据对象的锁状态动态改变。主要分为以下五种状态:
-
无锁时的 Mark Word
在对象未被锁定的情况下,Mark Word 的前 25bit 未使用。随后的 29bit 用于存储对象的 HashCode(如果调用了 hashCode() 方法),接着使用 4bit 存储对象的分代年龄(用于垃圾回收)。1bit 用于标识是否为偏向锁,在无锁状态下,这一位是 0(非偏向锁)。最后的 2bit 是固定的锁标志位 01。
-
有偏向锁的 Mark Word
当对象被偏向锁锁定时,Mark Word 的结构发生变化。前 54bit(25bit线程ID + 29bit Epoch)指向持有该锁的线程。Epoch 是偏向锁的时间戳,用于批量撤销。此时,偏向锁标识位被置为 1,锁标志位仍为 01。
-
有轻量级锁的 Mark Word
当对象被轻量级锁锁定时,Mark Word 的前 62bit 指向线程栈帧中的 LockRecord 对象(下文会详述)。你可以将 LockRecord 理解为线程持有该轻量级锁的凭证。锁标志位变为 00。
-
有重量级锁的 Mark Word
当对象被重量级锁锁定时,Mark Word 的前 62bit 指向一个重量级的监视器对象(如 ObjectMonitor)。锁标志位变为 10。
-
有 GC 标志的 Mark Word
当对象被垃圾收集器标记时,锁标志位会变为 11。
理解了 Mark Word,我们就可以深入锁升级的每一个环节了。
偏向锁
当一个线程首次成功获取锁时,JVM 会启用偏向锁。此时不存在竞争,性能极高。此锁会“偏向”第一个访问它的线程。之后该线程再次进入同步块时,无需任何同步操作(如 CAS),直接通行即可。
偏向锁在 JDK 15 及之后的版本中已被默认禁用并计划移除。主要原因在于其维护开销较高,且在实际高并发场景下,多线程竞争是常态,偏向锁在存在竞争时,撤销和升级的代价会抵消其带来的收益。因此,Java 团队更推荐使用轻量级锁或重量级锁:
- 轻量级锁:适用于竞争不激烈、线程持有锁时间较短的场景,通过 CAS 自旋等待。
- 重量级锁:适用于竞争激烈或线程持有锁时间较长的场景,避免大量 CPU 空转。
轻量级锁
当发生线程竞争,且竞争程度较轻时,偏向锁会升级为轻量级锁。这个竞争过程是通过 CAS 自旋完成的。
线程竞争轻量级锁的流程如下:
当线程准备进入 synchronized 代码块时,JVM 会先在当前线程的栈帧中创建一个名为 Lock Record 的空间。这个空间用于存储锁对象 Mark Word 的一个拷贝,官方称之为 Displaced Mark Word。
线程尝试获取锁时,会执行以下步骤:
- 将锁对象的 Mark Word 复制到自己的
Lock Record 中(即 Displaced Mark Word)。
- 接着,通过 CAS 操作 尝试将锁对象 Mark Word 的前 62bit 更新为指向自己栈帧中
Lock Record 的指针。
- 同时,将
Lock Record 内部的 owner 指针指向锁对象的 Mark Word。
如果 CAS 操作成功,则表明该线程成功获取了轻量级锁。随后,JVM 会将锁对象 Mark Word 的最后 2bit 锁标志位设置为 00。
如果 CAS 操作失败,JVM 会检查锁对象 Mark Word 中的指针是否已经指向了当前线程的栈帧。如果是,说明是重入,线程可以直接进入同步块;否则,说明有其他线程持有锁,当前线程将进入自旋等待。
下图描绘了轻量级锁状态下,线程栈中 Lock Record 与锁对象 Mark Word 的关系。
为什么需要设计 Lock Record 来存储 Mark Word 的拷贝?
核心目的是 为了保存对象的 HashCode。在无锁状态下,对象的 HashCode 直接存储在 Mark Word 的特定比特位中。
但是,当锁升级为偏向锁时,原本存储 HashCode 的位置需要用来存储持有锁的线程信息。这正是偏向锁与对象的 HashCode 不能共存的原因(调用 hashCode() 方法会导致偏向锁撤销)。
因此,Lock Record 充当了一个临时保管箱。在加轻量级锁时,先将对象的 Mark Word(包含HashCode)复制到 Lock Record 中保存。当锁被释放、对象恢复为无锁状态时,再将 Lock Record 中保存的 HashCode 信息写回对象的 Mark Word。下图展示了锁撤销时,HashCode 信息如何从 Lock Record 恢复到 Mark Word。
重量级锁
当线程 CAS 自旋获取轻量级锁失败达到一定次数(自适应自旋),锁就会膨胀升级为重量级锁。
重量级锁的抢占流程如下(了解整体流程即可,具体实现在 JVM 底层):
- 当锁准备升级为重量级锁时,JVM 会为锁对象关联一个
ObjectMonitor 对象。这个对象内部维护了两个关键队列:_EntryList(阻塞队列)和 _WaitSet(等待队列)。
- 如果锁正被其他线程占用,新来的竞争线程会进入
_EntryList 队列中阻塞等待。
- 当持有重量级锁的线程释放锁后,JVM 会从
_EntryList 中挑选一个线程(通常是指向头节点)作为 OnDeck Thread(候选线程),准备获取锁。
- 如果持有锁的线程调用了
Object.wait() 方法,它会释放锁并进入 _WaitSet 队列。当被 notify() 或 notifyAll() 唤醒后,线程会从 _WaitSet 转移到 _EntryList 中,重新参与锁竞争。
下图描述了 ObjectMonitor 中 _EntryList、_Owner、_WaitSet 的流转关系。
需要特别指出的是,synchronized 实现的重量级锁是 非公平锁。因为线程在进入阻塞队列前,会先尝试自旋(或轻量级锁的CAS)直接获取锁。如果成功,就“插队”获得了锁,这对于已经在队列中等待的线程来说是不公平的。
总结
最后我们来总结一下完整的锁升级流程:
- 无锁 -> 偏向锁:第一个线程访问时,将对象 Mark Word 中的线程ID指向自己,并置偏向模式。
- 偏向锁 -> 轻量级锁:当有另一个线程来竞争时,撤销偏向锁。两个(或多个)线程通过 CAS 自旋,竞争将 Mark Word 替换为指向各自栈中
Lock Record 的指针,成功者获得轻量级锁。
- 轻量级锁 -> 重量级锁:当 CAS 自旋竞争达到一定阈值,锁膨胀为重量级锁。未获得锁的线程进入
ObjectMonitor 的阻塞队列排队等待。
理解 synchronized 的锁升级机制,不仅对面试求职大有裨益,更是深入理解 JVM 并发原理和性能调优的重要基础。希望这篇解析能帮助你构建更清晰的知识图谱。如果你想与其他开发者交流更多技术细节,欢迎来云栈社区一起探讨。