文章目录

Java的synchronized锁升级与偏向锁撤销

发布于 2026-05-31 20:23:51 · 浏览 23 次 · 评论 0 条

Java的synchronized锁升级与偏向锁撤销

synchronized关键字是Java内置的同步机制。在JDK 1.6之前,其性能开销较大,因为每次加锁都是重量级操作。为了提升性能,JVM引入了锁升级机制,其中偏向锁是优化的第一步。然而,偏向锁并非“一劳永逸”,当出现竞争时,它会被撤销并升级。本文将手把手解析这一过程。


第一部分:理解锁的四种状态

在对象的对象头(Mark Word)中,锁状态通过特定的比特位来表示。从“无锁”到“重量级锁”,是一个逐渐升级以应对竞争的过程。

  1. 识别对象头中的锁标志位。这是理解升级的基础。对象头Mark Word的最后2-3个比特位决定了锁的状态:

    • 01无锁偏向锁状态。是否偏向取决于倒数第三个比特位是否为1
    • 00轻量级锁状态。
    • 10重量级锁状态。
  2. 明确四种状态及其设计目的

    • 无锁状态:对象刚被创建,未被任何线程锁定。
    • 偏向锁旨在消除无竞争情况下的同步开销。当第一个线程访问同步块时,会在对象头中记录该线程的ID,此后该线程进出同步块时,只需简单检查ID是否匹配,无需执行CAS(Compare-And-Swap)原子操作。
    • 轻量级锁旨在消除多线程交替执行(无实际竞争)时的互斥同步开销。当存在第二个线程尝试获取锁时,偏向锁会撤销并升级为轻量级锁。线程在栈帧中创建锁记录(Lock Record),通过CAS将对象头的Mark Word替换为指向锁记录的指针。
    • 重量级锁处理真正的多线程并发竞争。当轻量级锁自旋失败(竞争激烈)后升级而来。依赖操作系统底层的Mutex Lock实现,涉及线程的上下文切换,开销最大。

第二部分:锁升级的完整过程

锁升级是一个单向过程:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。我们以最常见的路径来拆解。

  1. 激活偏向锁模式。JVM默认开启偏向锁(参数-XX:+UseBiasedLocking)。当一个线程第一次进入同步块时:

    • 检查 对象头的Mark Word是否为可偏向状态(锁标志位为01,偏向位为0)。
    • 尝试 通过CAS操作将线程的ID写入对象头的Mark Word
    • 如果CAS成功,设置 对象头为偏向锁状态(偏向位置1),当前线程获得锁。此后,该线程再次进入该锁,只需检测 Mark Word中的线程ID是否为自身ID,即可快速获取锁。
  2. 触发偏向锁撤销并升级为轻量级锁。这是关键步骤。当另一个线程(线程B) 尝试获取已被偏向的锁时,升级开始:

    • 线程B发现 锁对象已是偏向锁状态,且指向线程A。
    • JVM暂停 线程A(在安全点Safepoint)。
    • 检查 线程A是否仍需要持有该锁。
      • 如果线程A的同步块已执行完毕或不再需要锁,则撤销 偏向锁,将对象头重置为无锁状态(01),然后线程B通过轻量级锁方式竞争。
      • 如果线程A仍需要锁,则升级:将对象头从偏向锁状态(01替换 为轻量级锁状态(00),将对象头的Mark Word内容复制到线程A栈帧中的锁记录里(称为Displaced Mark Word),然后让对象头指向线程A的锁记录。之后,线程A继续执行,线程B通过自旋尝试获取该轻量级锁。
  3. 轻量级锁膨胀为重量级锁。当存在多个线程自旋获取锁,或者自旋超过一定次数(JVM自适应自旋)仍未获得锁时:

    • JVM判断 该锁的竞争已变得激烈。
    • 创建 一个与锁对象关联的ObjectMonitor(监视器锁)对象,这是重量级锁的核心数据结构。
    • 设置 对象头的Mark Word指向ObjectMonitor,并将锁标志位改为10(重量级锁状态)。
    • 唤醒ObjectMonitorEntryList中等待的线程,让它们重新竞争锁。

第三部分:深入剖析偏向锁撤销

偏向锁的撤销是性能敏感操作,理解其细节至关重要。

  1. 明确撤销的触发条件。偏向锁在以下情况下会被撤销:

    • 当一个线程持有一个对象的偏向锁时,另一个线程尝试获取该锁。
    • 调用了对象的hashCode()方法。因为偏向锁模式下,对象头被线程ID占据,无法存储哈希码,计算哈希码会导致锁撤销。
    • 调用了wait()notify()方法,这些操作需要使用ObjectMonitor
  2. 理解撤销的执行时机——安全点Safepoint。偏向锁撤销操作不会立即发生。JVM会等待 到达一个安全点。安全点是代码中特定的、能让JVM安心进行一些操作的位置(如方法调用、循环跳转等)。在安全点,JVM会暂停 所有持有偏向锁的线程(Stop-The-World),然后检查执行 撤销逻辑。

  3. 分析撤销后的对象头状态。撤销完成后,根据之前的状态,对象头会被重置为无锁状态或直接升级为轻量级锁状态。

    • 重置为无锁(01):如果原持有锁的线程已不再需要锁,JVM会将对象头Mark Word中的线程ID、epoch等信息清除,使其回到一个可被重新偏向的初始状态。
    • 升级为轻量级锁(00):如果原持有锁的线程仍需要锁,则按照第二部分第2步所述,进行升级操作。
  4. 认识批量重偏向与批量撤销。为了优化性能,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输出格式,最后几位才是标志位,需要仔细对照其文档解读。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文