Java的synchronized锁升级与偏向锁撤销
synchronized关键字是Java内置的同步机制。在JDK 1.6之前,其性能开销较大,因为每次加锁都是重量级操作。为了提升性能,JVM引入了锁升级机制,其中偏向锁是优化的第一步。然而,偏向锁并非“一劳永逸”,当出现竞争时,它会被撤销并升级。本文将手把手解析这一过程。
第一部分:理解锁的四种状态
在对象的对象头(Mark Word)中,锁状态通过特定的比特位来表示。从“无锁”到“重量级锁”,是一个逐渐升级以应对竞争的过程。
-
识别对象头中的锁标志位。这是理解升级的基础。对象头
Mark Word的最后2-3个比特位决定了锁的状态:01:无锁或偏向锁状态。是否偏向取决于倒数第三个比特位是否为1。00:轻量级锁状态。10:重量级锁状态。
-
明确四种状态及其设计目的。
- 无锁状态:对象刚被创建,未被任何线程锁定。
- 偏向锁:旨在消除无竞争情况下的同步开销。当第一个线程访问同步块时,会在对象头中记录该线程的ID,此后该线程进出同步块时,只需简单检查ID是否匹配,无需执行CAS(Compare-And-Swap)原子操作。
- 轻量级锁:旨在消除多线程交替执行(无实际竞争)时的互斥同步开销。当存在第二个线程尝试获取锁时,偏向锁会撤销并升级为轻量级锁。线程在栈帧中创建锁记录(
Lock Record),通过CAS将对象头的Mark Word替换为指向锁记录的指针。 - 重量级锁:处理真正的多线程并发竞争。当轻量级锁自旋失败(竞争激烈)后升级而来。依赖操作系统底层的
Mutex Lock实现,涉及线程的上下文切换,开销最大。
第二部分:锁升级的完整过程
锁升级是一个单向过程:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。我们以最常见的路径来拆解。
-
激活偏向锁模式。JVM默认开启偏向锁(参数
-XX:+UseBiasedLocking)。当一个线程第一次进入同步块时:- 检查 对象头的
Mark Word是否为可偏向状态(锁标志位为01,偏向位为0)。 - 尝试 通过CAS操作将线程的ID写入对象头的
Mark Word。 - 如果CAS成功,设置 对象头为偏向锁状态(偏向位置
1),当前线程获得锁。此后,该线程再次进入该锁,只需检测Mark Word中的线程ID是否为自身ID,即可快速获取锁。
- 检查 对象头的
-
触发偏向锁撤销并升级为轻量级锁。这是关键步骤。当另一个线程(线程B) 尝试获取已被偏向的锁时,升级开始:
- 线程B发现 锁对象已是偏向锁状态,且指向线程A。
- JVM暂停 线程A(在安全点
Safepoint)。 - 检查 线程A是否仍需要持有该锁。
- 如果线程A的同步块已执行完毕或不再需要锁,则撤销 偏向锁,将对象头重置为无锁状态(
01),然后线程B通过轻量级锁方式竞争。 - 如果线程A仍需要锁,则升级:将对象头从偏向锁状态(
01)替换 为轻量级锁状态(00),将对象头的Mark Word内容复制到线程A栈帧中的锁记录里(称为Displaced Mark Word),然后让对象头指向线程A的锁记录。之后,线程A继续执行,线程B通过自旋尝试获取该轻量级锁。
- 如果线程A的同步块已执行完毕或不再需要锁,则撤销 偏向锁,将对象头重置为无锁状态(
-
轻量级锁膨胀为重量级锁。当存在多个线程自旋获取锁,或者自旋超过一定次数(JVM自适应自旋)仍未获得锁时:
- JVM判断 该锁的竞争已变得激烈。
- 创建 一个与锁对象关联的
ObjectMonitor(监视器锁)对象,这是重量级锁的核心数据结构。 - 设置 对象头的
Mark Word指向ObjectMonitor,并将锁标志位改为10(重量级锁状态)。 - 唤醒 在
ObjectMonitor的EntryList中等待的线程,让它们重新竞争锁。
第三部分:深入剖析偏向锁撤销
偏向锁的撤销是性能敏感操作,理解其细节至关重要。
-
明确撤销的触发条件。偏向锁在以下情况下会被撤销:
- 当一个线程持有一个对象的偏向锁时,另一个线程尝试获取该锁。
- 调用了对象的
hashCode()方法。因为偏向锁模式下,对象头被线程ID占据,无法存储哈希码,计算哈希码会导致锁撤销。 - 调用了
wait()或notify()方法,这些操作需要使用ObjectMonitor。
-
理解撤销的执行时机——安全点
Safepoint。偏向锁撤销操作不会立即发生。JVM会等待 到达一个安全点。安全点是代码中特定的、能让JVM安心进行一些操作的位置(如方法调用、循环跳转等)。在安全点,JVM会暂停 所有持有偏向锁的线程(Stop-The-World),然后检查 和执行 撤销逻辑。 -
分析撤销后的对象头状态。撤销完成后,根据之前的状态,对象头会被重置为无锁状态或直接升级为轻量级锁状态。
- 重置为无锁(01):如果原持有锁的线程已不再需要锁,JVM会将对象头
Mark Word中的线程ID、epoch等信息清除,使其回到一个可被重新偏向的初始状态。 - 升级为轻量级锁(00):如果原持有锁的线程仍需要锁,则按照第二部分第2步所述,进行升级操作。
- 重置为无锁(01):如果原持有锁的线程已不再需要锁,JVM会将对象头
-
认识批量重偏向与批量撤销。为了优化性能,JVM对以类(
class)为单位的偏向锁进行批量处理。- 批量重偏向:当一个类的对象被频繁撤销偏向锁时,JVM会判定 该类的竞争模式发生了变化。它会重置 该类所有对象的偏向锁 epoch值。之后,当持有该类对象的线程再次访问它们时,可以快速重新偏向 到当前线程,而无需再经过撤销流程。
- 批量撤销:如果经过多次批量重偏向后,该类对象的锁竞争依然非常激烈,JVM会决定 彻底禁用 该类的偏向锁。此后,该类所有新创建或已解锁的对象,都会直接跳过偏向锁状态,进入无锁或轻量级锁状态。
代码视角:观察锁状态变化
虽然不能直接查看对象头,但可以通过JOL(Java Object Layout)库来观察对象头的变化。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
import org.openjdk.jol.info.ClassLayout;
public class LockUpgradeDemo {
public static void main(String[] args) throws Exception {
Object lock = new Object();
// 1. 打印对象头(无锁状态)
System.out.println("=== Before Lock ===");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
// 2. 启用偏向锁并进入同步块
System.out.println("=== After First Thread Lock ===");
synchronized (lock) {
// 此时对象头应为偏向锁状态,包含主线程ID
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
// 3. 模拟另一个线程尝试获取锁,触发撤销和升级
Thread t = new Thread(() -> {
synchronized (lock) {
// 暂停主线程,确保锁状态变化被观察到
try { Thread.sleep(2000); } catch (InterruptedException e) {}
System.out.println("=== Inside Second Thread ===");
// 此时对象头应为轻量级锁状态(00)
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
});
t.start();
// 等待第二个线程启动并获取锁
Thread.sleep(1000);
System.out.println("=== After Second Thread Started ===");
// 此时主线程可能仍在等待锁(取决于调度),对象头应为轻量级锁状态(00)
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
运行代码并仔细观察输出中对象头标志位(mark)的变化,可以直观地看到从001(偏向锁)到000(轻量级锁)的转换过程。注意,由于JOL输出格式,最后几位才是标志位,需要仔细对照其文档解读。

暂无评论,快来抢沙发吧!