首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >synchronized 锁升级全流程

synchronized 锁升级全流程

作者头像
果酱带你啃java
发布2026-04-14 15:00:11
发布2026-04-14 15:00:11
410
举报

在Java并发编程领域,synchronized无疑是最基础也最核心的同步工具。从JDK早期的“重量级锁”标签,到JDK1.6引入锁升级机制带来的性能飞跃,再到JDK15之后对偏向锁的默认禁用,synchronized的优化从未停止。本文将基于JDK17 LTS版本,从底层对象布局、JVM源码实现几个维度,100%准确地拆解synchronized的锁升级全流程,让你彻底搞懂无锁、偏向锁、轻量级锁、重量级锁的底层逻辑,既能夯实并发编程的核心基础,也能解决生产环境中的实际并发问题。

一、前置核心知识:Java对象布局与Mark Word

整个synchronized锁升级机制,完全是围绕Java对象头中的Mark Word实现的,想要彻底搞懂锁升级,必须先吃透Java对象的内存布局。

在64位JVM(当前生产环境主流)开启压缩指针(-XX:+UseCompressedOops,JDK17默认开启)的情况下,Java对象在堆内存中的布局分为三个部分:

  1. 对象头(Object Header):分为Mark Word(标记字段,8字节)、Class Pointer(类型指针,4字节,压缩后);如果是数组对象,额外增加4字节的数组长度。
  2. 实例数据(Instance Data):对象的成员变量(包括父类继承的),按照8字节对齐规则排列。
  3. 对齐填充(Padding):保证整个对象的大小是8字节的整数倍,满足JVM的内存对齐要求。

核心重点:Mark Word动态数据结构

Mark Word是一个8字节(64位)的动态数据结构,会根据对象的运行状态复用存储空间,不同状态下的位分布完全不同,这是锁升级的核心载体。以下是64位JVM开启压缩指针后,Mark Word在不同锁状态下的权威位分布(基于OpenJDK 17源码):

锁状态

64位Mark Word位分布(从高位到低位)

偏向锁标志(1位)

锁标志(2位)

无锁不可偏向

unused(25) | identityHashCode(31) | unused(1) | 分代年龄(4)

0

01

无锁可偏向

thread(54) | epoch(2) | unused(1) | 分代年龄(4)

1

01

偏向锁

偏向线程ID(54) | epoch(2) | unused(1) | 分代年龄(4)

1

01

轻量级锁

指向线程栈Lock Record的指针(62)

00

重量级锁

指向ObjectMonitor对象的指针(62)

10

GC标记

空(62)

11

这里必须明确两个核心判断位,也是很多技术文章的常见错误点:

  • lock位(2位):决定锁的基础状态,仅靠这一位无法区分无锁和偏向锁。
  • biased_lock位(1位):与lock位配合,01+0为无锁不可偏向,01+1为可偏向/偏向锁状态。

二、synchronized的使用场景与底层JVM实现

2.1 三种核心使用场景

synchronized的本质是对某个对象加锁,根据锁对象的不同,分为三种使用场景:

  1. 修饰实例方法:锁对象为当前实例this,属于对象锁,不同实例的锁互不影响。
  2. 修饰静态方法:锁对象为当前类的Class对象,属于类锁,对该类的所有实例生效。
  3. 修饰代码块:锁对象为括号内指定的对象,可自定义锁粒度,是生产环境推荐的用法。

2.2 底层JVM实现原理

根据JVM规范,synchronized的底层实现分为两种形式,本质都是获取对象对应的监视器锁:

  1. 同步代码块:通过monitorentermonitorexit字节码指令实现。线程执行到monitorenter时尝试获取对象的监视器所有权,执行到monitorexit时释放所有权,编译器会保证异常时也能执行monitorexit,避免死锁。
  2. 同步方法:通过方法修饰符中的ACC_SYNCHRONIZED标志实现。JVM调用方法时会检查该标志,若开启则自动尝试获取对象的监视器锁,方法执行完毕后自动释放,语义与monitorenter/monitorexit完全一致。

所有Java对象都天然关联一个ObjectMonitor监视器对象,这是重量级锁的核心载体,而偏向锁、轻量级锁不会直接初始化该对象,这也是不同锁类型的核心性能差异来源。

三、锁升级全流程核心原理

synchronized的锁升级是JVM为了减少同步开销而做的自适应优化,核心逻辑是:根据竞争激烈程度,从低开销锁逐步升级到高开销锁,锁的膨胀过程在持有期间是单向的,不可降级,锁完全释放后会重置为无锁状态。

锁升级全流程总览

3.1 第一阶段:无锁状态

无锁状态分为两种子状态,是锁升级的起点:

  1. 无锁可偏向状态:开启偏向锁的前提下,对象刚创建,未调用过System.identityHashCode()Mark Wordbiased_lock=1lock=01,线程ID、epoch字段全为0,可被第一个获取锁的线程偏向。
  2. 无锁不可偏向状态:关闭偏向锁,或对象已调用过identityHashCode()Mark Wordbiased_lock=0lock=01,存储了对象的哈希值,无法进入偏向锁状态,获取锁时直接进入轻量级锁流程。
核心知识点:为什么调用identityHashCode会禁用偏向锁?

64位JVM中,对象的identityHashCode是31位,正好占用了偏向锁状态下存储线程ID的54位空间的低31位。一旦写入哈希值,就没有足够空间存储偏向线程ID,JVM会强制禁用该对象的偏向锁,这是底层位空间冲突导致的必然结果,后续会通过代码实战验证该逻辑。

3.2 第二阶段:偏向锁

核心设计思想

在无多线程竞争的场景下,完全消除同步操作的开销,让锁永久偏向于第一个获取它的线程。后续该线程重入同步块时,无需任何CAS操作,仅需简单的字段检查,性能几乎与无锁代码一致。

JDK17特殊说明

根据JEP 374规范,JDK15及以上版本默认禁用偏向锁-XX:+UseBiasedLocking参数被标记为废弃,-XX:BiasedLockingStartupDelay默认值为0。若要开启偏向锁,需在JVM启动参数中添加:

代码语言:javascript
复制
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 --add-opens java.base/java.lang=ALL-UNNAMED

其中--add-opens参数用于JOL工具查看对象头,适配JDK17的模块化机制。

偏向锁的获取流程
  1. 线程进入同步块时,检查对象Mark Wordbiased_lock=1lock=01,确认处于可偏向状态。
  2. 检查Mark Word中的线程ID字段:
    • 若为当前线程ID,说明已持有偏向锁,直接进入同步块,无任何额外开销。
    • 若为空,说明无线程持有偏向锁,当前线程通过CAS操作将自身线程ID写入Mark Word
  3. CAS成功:当前线程获得偏向锁,后续重入无需任何同步操作。
  4. CAS失败:说明有其他线程已竞争该锁,触发偏向锁撤销流程。
偏向锁的撤销机制

偏向锁的撤销是较重的操作,需要等待全局安全点(STW),暂停所有持有该锁的线程,检查持有偏向锁的线程状态:

  1. 若线程已死亡,直接将Mark Word重置为无锁可偏向状态。
  2. 若线程仍存活,遍历该线程的栈帧,检查是否仍在使用该锁:
    • 仍在使用:将偏向锁升级为轻量级锁,把Displaced Mark Word写入线程的Lock RecordMark Word改为指向Lock Record的指针,lock位改为00。
    • 已不再使用:将Mark Word重置为无锁可偏向状态,完成撤销。
批量重偏向与批量撤销

为了避免大量对象的偏向锁频繁撤销导致的STW开销,JVM引入了批量优化机制:

  • 批量重偏向:当某个类的对象偏向锁撤销次数达到-XX:BiasedLockingBulkRebiasThreshold(默认20),JVM会给该类的epoch字段加1,所有该类对象的偏向锁需重新偏向,解决同类型对象在多线程间交替使用导致的频繁撤销问题。
  • 批量撤销:当某个类的对象偏向锁撤销次数达到-XX:BiasedLockingBulkRevokeThreshold(默认40),JVM会判定该类存在持续的多线程竞争,直接禁用该类所有对象的偏向锁,后续创建的该类对象均为不可偏向状态。

3.3 第三阶段:轻量级锁

核心设计思想

在多线程交替使用锁、无同时竞争的场景下,避免重量级锁的用户态与内核态切换开销,基于线程栈帧中的Lock Record和CAS操作实现,属于用户态自旋锁,开销远小于重量级锁。

轻量级锁的获取流程
  1. 线程进入同步块时,若对象处于无锁不可偏向状态,或偏向锁撤销后需升级,JVM会在当前线程的栈帧中创建Lock Record空间,用于存储对象当前Mark Word的拷贝,该拷贝称为Displaced Mark Word
  2. 线程通过CAS操作,尝试将对象的Mark Word替换为指向当前线程栈帧中Lock Record的指针。
  3. CAS成功:当前线程获得轻量级锁,将Mark Wordlock位设置为00,执行同步代码。
  4. CAS失败:说明其他线程已持有该对象的轻量级锁,当前线程进入自适应自旋阶段,不断重试CAS操作尝试获取锁。
自适应自旋机制

JDK17中的自旋锁是完全自适应的,JVM会根据前一次在同一个锁上的自旋时间、锁持有者的状态,动态调整自旋次数:

  • 若前一次自旋成功获取了锁,JVM会认为本次自旋大概率成功,增加自旋次数。
  • 若前一次自旋失败,JVM会减少自旋次数,甚至直接跳过自旋升级为重量级锁,避免CPU空转浪费资源。
轻量级锁的释放流程
  1. 线程执行完同步代码后,通过CAS操作,将Lock Record中存储的Displaced Mark Word写回对象的Mark Word
  2. CAS成功:锁释放完成,对象回到无锁状态。
  3. CAS失败:说明自旋过程中锁已被升级为重量级锁,需进入重量级锁的释放流程,唤醒等待队列中的线程。
适用场景

轻量级锁适用于多线程交替执行同步块、锁持有时间短、无同时竞争的场景。若多个线程同时竞争锁,轻量级锁的自旋会消耗大量CPU资源,性能反而会急剧下降,此时JVM会将锁升级为重量级锁。

3.4 第四阶段:重量级锁

核心设计思想

在多线程同时激烈竞争锁的场景下,通过操作系统的互斥量(mutex)实现同步,避免CPU空转,保证线程安全。

核心载体:ObjectMonitor

重量级锁的底层是ObjectMonitor对象,其核心结构来自OpenJDK 17源码:

  • _owner:指向当前持有锁的线程。
  • _EntryList:竞争锁失败的线程进入该队列,处于BLOCKED阻塞状态。
  • _WaitSet:调用了wait()方法的线程进入该队列,处于WAITING等待状态。
  • _count:锁的重入次数。
  • _recursions:锁的重入深度。
重量级锁的获取流程
  1. 当轻量级锁的自适应自旋多次失败,CAS仍未成功,JVM会将对象的锁升级为重量级锁,初始化ObjectMonitor对象,将对象的Mark Word设置为指向ObjectMonitor的指针,lock位改为10。
  2. 竞争锁的线程进入ObjectMonitor_EntryList队列,线程状态从RUNNABLE变为BLOCKED,操作系统将线程挂起,发生用户态到内核态的切换
  3. 持有锁的线程释放锁时,会从_EntryList中唤醒一个线程,重新竞争锁。
  4. 重入机制:持有锁的线程可多次重入,_count_recursions字段会记录重入次数,释放时需对应次数的monitorexit,直到_count为0,锁才会真正释放。
性能开销说明

重量级锁的核心开销来自用户态与内核态的切换,线程的阻塞和唤醒需要操作系统内核完成,涉及进程上下文的保存和恢复,开销极大。因此重量级锁适用于锁持有时间长、竞争激烈的场景,此时切换开销远小于CPU空转的开销。

四、锁升级全流程实战代码验证

以下所有代码均基于JDK17编写,用于验证上述锁升级的核心原理。

4.1 项目依赖配置(pom.xml)

采用所有组件的最新稳定版本,符合生产环境规范:

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jam</groupId>
    <artifactId>synchronized-demo</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <lombok.version>1.18.34</lombok.version>
        <jol.version>0.17</jol.version>
        <slf4j.version>2.0.12</slf4j.version>
        <logback.version>1.5.6</logback.version>
        <spring.version>6.1.15</spring.version>
        <fastjson2.version>2.0.52</fastjson2.version>
        <guava.version>33.1.0-jre</guava.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>${jol.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4.2 无锁状态与对象头验证

代码语言:javascript
复制
package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import org.springframework.util.ObjectUtils;

/**
 * 无锁状态对象头验证示例
 *
 * @author ken
 * @date 2026-03-05
 */
@Slf4j
publicclass NoLockDemo {

    privatestaticfinal Object LOCK_OBJECT = new Object();

    public static void main(String[] args) {
        log.info("===== 刚创建的对象,无锁状态 =====");
        printObjectHeader(LOCK_OBJECT);

        log.info("===== 调用identityHashCode()之后的对象头 =====");
        int identityHashCode = System.identityHashCode(LOCK_OBJECT);
        log.info("对象的identityHashCode: {}", Integer.toHexString(identityHashCode));
        printObjectHeader(LOCK_OBJECT);
    }

    /**
     * 打印对象头信息
     *
     * @param obj 待打印的对象
     */
    private static void printObjectHeader(Object obj) {
        if (ObjectUtils.isEmpty(obj)) {
            log.error("打印对象头失败,对象为空");
            return;
        }
        String classLayout = ClassLayout.parseInstance(obj).toPrintable();
        log.info("\n{}", classLayout);
    }
}

运行说明:需添加JVM参数--add-opens java.base/java.lang=ALL-UNNAMED,运行后可观察到:刚创建的对象Mark Word的偏向锁标志位,调用identityHashCode后,哈希值被写入Mark Word,偏向锁标志位变为0,对象进入不可偏向状态。

4.3 偏向锁状态验证

代码语言:javascript
复制
package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import org.springframework.util.ObjectUtils;

/**
 * 偏向锁状态验证示例
 * 需添加JVM启动参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 --add-opens java.base/java.lang=ALL-UNNAMED
 *
 * @author ken
 * @date 2026-03-05
 */
@Slf4j
publicclass BiasedLockDemo {

    privatestaticfinal Object LOCK_OBJECT = new Object();

    public static void main(String[] args) throws InterruptedException {
        log.info("===== 刚创建的对象,可偏向无锁状态 =====");
        printObjectHeader(LOCK_OBJECT);

        log.info("===== 主线程第一次获取锁,偏向锁获取 =====");
        synchronized (LOCK_OBJECT) {
            printObjectHeader(LOCK_OBJECT);
        }

        log.info("===== 主线程释放锁之后,偏向锁状态保持 =====");
        printObjectHeader(LOCK_OBJECT);

        log.info("===== 主线程第二次重入锁,偏向锁重入 =====");
        synchronized (LOCK_OBJECT) {
            printObjectHeader(LOCK_OBJECT);
        }

        log.info("===== 新线程竞争锁,触发偏向锁撤销 =====");
        Thread thread = new Thread(() -> {
            synchronized (LOCK_OBJECT) {
                log.info("===== 新线程获取锁,锁升级为轻量级锁 =====");
                printObjectHeader(LOCK_OBJECT);
            }
        });
        thread.start();
        thread.join();

        log.info("===== 偏向锁撤销后,对象最终状态 =====");
        printObjectHeader(LOCK_OBJECT);
    }

    /**
     * 打印对象头信息
     *
     * @param obj 待打印的对象
     */
    private static void printObjectHeader(Object obj) {
        if (ObjectUtils.isEmpty(obj)) {
            log.error("打印对象头失败,对象为空");
            return;
        }
        String classLayout = ClassLayout.parseInstance(obj).toPrintable();
        log.info("\n{}", classLayout);
    }
}

运行结果:开启偏向锁后,主线程第一次获取锁时,Mark Word中写入主线程ID,进入偏向锁状态;释放锁后偏向锁状态保持,重入无额外开销;新线程竞争锁触发偏向锁撤销,锁升级为轻量级锁,lock位变为00。

4.4 轻量级锁状态验证

代码语言:javascript
复制
package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import org.springframework.util.ObjectUtils;

/**
 * 轻量级锁状态验证示例
 * 需添加JVM启动参数:--add-opens java.base/java.lang=ALL-UNNAMED
 *
 * @author ken
 * @date 2026-03-05
 */
@Slf4j
publicclass LightweightLockDemo {

    privatestaticfinal Object LOCK_OBJECT = new Object();
    privatestaticint count = 0;

    public static void main(String[] args) throws InterruptedException {
        log.info("===== 初始无锁状态 =====");
        printObjectHeader(LOCK_OBJECT);

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronized (LOCK_OBJECT) {
                    count++;
                    log.info("线程1执行同步代码,当前count: {}", count);
                    if (i == 2) {
                        log.info("===== 线程1持有锁,轻量级锁状态 =====");
                        printObjectHeader(LOCK_OBJECT);
                    }
                }
            }
        }, "thread-1");

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronized (LOCK_OBJECT) {
                    count++;
                    log.info("线程2执行同步代码,当前count: {}", count);
                    if (i == 2) {
                        log.info("===== 线程2持有锁,轻量级锁状态 =====");
                        printObjectHeader(LOCK_OBJECT);
                    }
                }
            }
        }, "thread-2");

        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();

        log.info("===== 所有线程执行完毕,最终count: {}, 锁状态 =====", count);
        printObjectHeader(LOCK_OBJECT);
    }

    /**
     * 打印对象头信息
     *
     * @param obj 待打印的对象
     */
    private static void printObjectHeader(Object obj) {
        if (ObjectUtils.isEmpty(obj)) {
            log.error("打印对象头失败,对象为空");
            return;
        }
        String classLayout = ClassLayout.parseInstance(obj).toPrintable();
        log.info("\n{}", classLayout);
    }
}

运行结果:两个线程交替执行同步代码,无同时竞争,锁保持轻量级锁状态,lock位为00,Mark Word中存储指向持有锁线程栈帧中Lock Record的指针。

4.5 重量级锁状态验证

代码语言:javascript
复制
package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import org.springframework.util.ObjectUtils;

import java.util.concurrent.CountDownLatch;

/**
 * 重量级锁状态验证示例
 * 需添加JVM启动参数:--add-opens java.base/java.lang=ALL-UNNAMED
 *
 * @author ken
 * @date 2026-03-05
 */
@Slf4j
publicclass HeavyweightLockDemo {

    privatestaticfinal Object LOCK_OBJECT = new Object();
    privatestaticint count = 0;
    privatestaticfinalint THREAD_COUNT = 10;
    privatestaticfinalint LOOP_COUNT = 1000;

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch startLatch = new CountDownLatch(1);
        CountDownLatch endLatch = new CountDownLatch(THREAD_COUNT);

        log.info("===== 初始无锁状态 =====");
        printObjectHeader(LOCK_OBJECT);

        for (int i = 0; i < THREAD_COUNT; i++) {
            new Thread(() -> {
                try {
                    startLatch.await();
                    for (int j = 0; j < LOOP_COUNT; j++) {
                        synchronized (LOCK_OBJECT) {
                            count++;
                            if (count == 5000) {
                                log.info("===== 多线程激烈竞争,锁升级为重量级锁 =====");
                                printObjectHeader(LOCK_OBJECT);
                            }
                        }
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    log.error("线程执行异常", e);
                } finally {
                    endLatch.countDown();
                }
            }, "thread-" + i).start();
        }

        startLatch.countDown();
        endLatch.await();

        log.info("===== 所有线程执行完毕,最终count: {}, 锁最终状态 =====", count);
        printObjectHeader(LOCK_OBJECT);
    }

    /**
     * 打印对象头信息
     *
     * @param obj 待打印的对象
     */
    private static void printObjectHeader(Object obj) {
        if (ObjectUtils.isEmpty(obj)) {
            log.error("打印对象头失败,对象为空");
            return;
        }
        String classLayout = ClassLayout.parseInstance(obj).toPrintable();
        log.info("\n{}", classLayout);
    }
}

运行结果:10个线程同时竞争锁,自适应自旋快速失败,锁升级为重量级锁,lock位变为10,Mark Word中存储指向ObjectMonitor对象的指针,最终count值为10000,保证线程安全。

4.6 identityHashCode导致偏向锁失效验证

代码语言:javascript
复制
package com.jam.demo;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import org.springframework.util.ObjectUtils;

/**
 * 调用identityHashCode后偏向锁失效验证示例
 * 需添加JVM启动参数:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 --add-opens java.base/java.lang=ALL-UNNAMED
 *
 * @author ken
 * @date 2026-03-05
 */
@Slf4j
publicclass BiasedLockInvalidDemo {

    privatestaticfinal Object LOCK_OBJECT_1 = new Object();
    privatestaticfinal Object LOCK_OBJECT_2 = new Object();

    public static void main(String[] args) {
        log.info("===== 锁对象1:刚创建,可偏向状态 =====");
        printObjectHeader(LOCK_OBJECT_1);

        log.info("===== 锁对象1:获取锁,进入偏向锁状态 =====");
        synchronized (LOCK_OBJECT_1) {
            printObjectHeader(LOCK_OBJECT_1);
        }

        log.info("===== 锁对象2:刚创建,可偏向状态 =====");
        printObjectHeader(LOCK_OBJECT_2);

        log.info("===== 锁对象2:调用identityHashCode =====");
        int identityHashCode = System.identityHashCode(LOCK_OBJECT_2);
        log.info("锁对象2的identityHashCode: {}", Integer.toHexString(identityHashCode));
        printObjectHeader(LOCK_OBJECT_2);

        log.info("===== 锁对象2:获取锁,无法进入偏向锁,直接进入轻量级锁 =====");
        synchronized (LOCK_OBJECT_2) {
            printObjectHeader(LOCK_OBJECT_2);
        }
    }

    /**
     * 打印对象头信息
     *
     * @param obj 待打印的对象
     */
    private static void printObjectHeader(Object obj) {
        if (ObjectUtils.isEmpty(obj)) {
            log.error("打印对象头失败,对象为空");
            return;
        }
        String classLayout = ClassLayout.parseInstance(obj).toPrintable();
        log.info("\n{}", classLayout);
    }
}

运行结果:锁对象1正常进入偏向锁状态;锁对象2调用identityHashCode后,偏向锁标志位变为0,获取锁时直接进入轻量级锁状态,验证了位空间冲突导致偏向锁失效的核心原理。

五、核心细节与易混淆点辨析

5.1 锁升级是完全不可逆的吗?

这是最常见的认知错误。准确结论是:锁的膨胀过程在持有期间是单向的,只能从低级别升级到高级别,不可降级;但当锁完全释放后,对象的Mark Word会被重置为无锁状态,下一次竞争会重新开始锁升级流程。例如,一个对象的锁升级为重量级锁后,当所有线程都释放锁,对象会回到无锁状态,下一次线程获取锁时,会重新从偏向锁(开启时)或轻量级锁开始。

5.2 三种锁类型核心区别对比

锁类型

核心实现

适用场景

性能开销

线程安全保障

偏向锁

Mark Word存储偏向线程ID,无CAS操作

单线程长期使用锁,无多线程竞争

几乎为0,与无锁代码一致

无竞争下线程安全

轻量级锁

线程栈帧Lock Record + 自适应CAS自旋

多线程交替使用锁,持有时间短,无同时竞争

用户态CAS操作,开销小,自旋失败消耗CPU

交替竞争下线程安全

重量级锁

操作系统互斥量mutex + ObjectMonitor

多线程同时激烈竞争,锁持有时间长

用户态与内核态切换,开销极大

激烈竞争下线程安全

5.3 轻量级锁和自旋锁的关系?

轻量级锁获取失败后会采用自旋重试,但自旋锁不等于轻量级锁。自旋是一种获取锁的重试机制,不仅轻量级锁会使用,重量级锁在进入阻塞之前也会尝试自旋。轻量级锁的核心是基于Lock Record的CAS操作,自旋是其竞争失败后的优化手段。

5.4 对象锁和类锁的核心区别?

  • 对象锁:锁对象为实例对象,不同实例的对象锁互不影响,synchronized修饰实例方法、synchronized(this)均为对象锁。
  • 类锁:锁对象为类的Class对象,一个类仅有一个Class对象,因此类锁对该类的所有实例生效,synchronized修饰静态方法、synchronized(XXX.class)均为类锁。
  • 本质上,两种锁的底层锁升级机制完全一致,仅锁对象不同。

5.5 为什么wait/notify必须在synchronized同步块中调用?

这三个方法均基于ObjectMonitor实现:调用wait()会释放锁并将线程加入WaitSet队列,调用notify()会唤醒WaitSet中的线程,这些操作都必须先持有对象的监视器锁。若不在同步块中调用,JVM会抛出IllegalMonitorStateException异常,这是JVM规范的强制要求,也是为了保证线程间通信的原子性和线程安全。

5.6 JDK17与JDK8中synchronized的核心区别?

  1. 偏向锁:JDK8默认开启偏向锁,JDK17默认禁用偏向锁,相关参数被标记为废弃。
  2. 自旋优化:JDK17的自适应自旋算法更智能,能根据运行时情况动态调整自旋次数,减少CPU资源浪费。
  3. 内存屏障:JDK17对synchronized的内存屏障语义做了优化,更贴合JMM规范,多核CPU下性能表现更好。
  4. 偏向锁优化:JDK17优化了批量重偏向/撤销机制,减少了STW时间。

六、生产环境最佳实践

6.1 锁粒度最小化原则

在保证线程安全的前提下,尽量缩小锁的范围,仅对需要同步的共享变量操作加锁,不要对整个方法加锁,减少锁的持有时间,降低竞争概率,避免锁快速升级为重量级锁。

  • 反例:整个方法加synchronized,内部包含大量非线程安全的耗时IO操作。
  • 正例:仅对共享变量的修改操作加锁,耗时操作移出同步块。

6.2 避免在同步块中执行耗时操作

同步块中若包含IO操作、网络调用、数据库查询等耗时操作,会导致锁持有时间大幅增加,多线程竞争概率急剧上升,锁会快速升级为重量级锁,性能严重下降。若必须执行耗时操作,尽量将其移出同步块。

6.3 合理选择锁对象

  • 锁对象必须是private final修饰的自定义对象,保证锁的封装性,避免外部代码获取锁对象导致死锁。
  • 禁止用StringInteger等包装类型作为锁对象,常量池缓存会导致锁对象被复用,引发非预期的线程安全问题。
  • 禁止用this作为锁对象,外部代码可获取到该实例,存在死锁风险。

6.4 JVM参数优化建议

  • 高并发激烈竞争场景:保持JDK17默认配置,关闭偏向锁,避免偏向锁频繁撤销带来的STW开销。
  • 单线程低竞争场景:可开启偏向锁,减少同步开销,提升性能。
  • 自旋次数:不建议手动调整自旋次数,JDK17的自适应自旋已高度优化,手动调整反而可能导致性能下降。

6.5 利用JIT的锁优化机制

JIT编译器会对synchronized做两项核心优化,可通过合理编码提升优化效果:

  • 锁消除:JIT通过逃逸分析,发现对象不会逃逸出当前线程,会直接消除该对象的同步锁。尽量减少对象逃逸,提升锁消除概率。
  • 锁粗化:JIT发现相邻的多个同步块使用同一个锁对象,会将其合并为一个同步块,减少多次加锁解锁的开销。避免在循环中频繁加锁解锁,尽量将同步块放在循环外部。

6.6 死锁规避规范

  • 固定加锁顺序:所有线程按照统一的顺序获取锁,避免循环等待。
  • 避免锁嵌套:尽量不要在一个同步块中获取另一个锁,减少死锁概率。
  • 超时控制:若使用可重入锁,可设置tryLock超时时间,避免无限期等待。

七、高频面试题与标准答案

  1. 简述synchronized的锁升级全流程答:synchronized的锁升级是JVM为减少同步开销做的自适应优化,基于对象的Mark Word实现,流程如下:
  • 无锁状态:对象刚创建,未被任何线程锁定,分为可偏向和不可偏向两种子状态。
  • 偏向锁:开启偏向锁的前提下,第一个线程获取锁时,通过CAS将自身线程ID写入Mark Word,锁偏向于该线程,后续重入无需任何同步操作,开销极小。
  • 轻量级锁:出现多线程交替竞争锁时,偏向锁被撤销,升级为轻量级锁。线程在栈帧中创建Lock Record,通过CAS将对象的Mark Word替换为指向Lock Record的指针,获取成功则持有锁,失败则进入自适应自旋。
  • 重量级锁:自旋多次失败,多线程同时激烈竞争锁时,锁升级为重量级锁,底层基于操作系统互斥量实现,竞争失败的线程会被阻塞,发生用户态到内核态的切换,开销极大。 锁的膨胀过程在持有期间是单向的,不可降级,锁完全释放后会重置为无锁状态。
  1. synchronized和ReentrantLock的核心区别答:两者都是可重入的独占锁,核心区别如下:
  • 底层实现:synchronized是JVM层面的关键字,基于对象头和ObjectMonitor实现;ReentrantLock是JDK层面的API,基于AQS抽象队列同步器实现。
  • 灵活性:synchronized的加锁解锁是自动的,无法手动控制;ReentrantLock可手动控制加锁解锁,支持尝试加锁、超时加锁、可中断加锁,灵活性更高。
  • 功能特性:ReentrantLock支持公平锁和非公平锁(默认非公平),synchronized仅支持非公平锁;ReentrantLock支持多个条件变量Condition,synchronized仅支持一个wait/notify队列。
  • 性能:低竞争场景下,synchronized的偏向锁、轻量级锁性能更好;高竞争场景下,ReentrantLock性能更稳定,JDK17中两者性能差距已极小。
  • 异常处理:synchronized会在异常时自动释放锁,不会导致死锁;ReentrantLock必须在finally块中手动释放锁,否则会导致死锁。
  1. 为什么调用identityHashCode会导致对象无法进入偏向锁状态?答:64位JVM中,对象的identityHashCode是31位,正好占用了偏向锁状态下存储偏向线程ID的54位空间的低31位。一旦调用identityHashCodeMark Word中会写入31位哈希值,没有足够空间存储偏向线程ID,因此JVM会强制禁用该对象的偏向锁,后续该对象获取锁时会直接进入轻量级锁流程。
  2. 什么是锁消除和锁粗化?答:两者都是JIT编译器对synchronized的优化手段:
  • 锁消除:JIT通过逃逸分析,判断某个锁对象不会逃逸出当前线程,不会被其他线程访问,就会直接消除该同步锁,避免不必要的加锁解锁开销。
  • 锁粗化:JIT发现相邻的多个同步块使用同一个锁对象,会将这些同步块合并为一个大的同步块,减少多次加锁解锁的开销,避免循环中频繁加锁解锁导致的性能损耗。
  1. synchronized的可重入性是如何实现的?答:synchronized的可重入性基于底层的计数器实现:
  • 偏向锁/轻量级锁:通过线程栈帧中的Lock Record数量记录重入次数,每重入一次创建一个新的Lock Record,释放时依次移除,直到所有Lock Record都被移除,锁才真正释放。
  • 重量级锁:通过ObjectMonitor中的_count_recursions字段记录重入次数,线程每重入一次,计数器加1,释放时减1,直到计数器为0,锁才真正释放。

结尾

synchronized是Java并发编程的基石,锁升级机制是其性能优化的核心。只有彻底理解其底层实现原理,才能写出高效、安全的并发代码,从根源上规避生产环境中的并发问题。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-03-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前置核心知识:Java对象布局与Mark Word
    • 核心重点:Mark Word动态数据结构
  • 二、synchronized的使用场景与底层JVM实现
    • 2.1 三种核心使用场景
    • 2.2 底层JVM实现原理
  • 三、锁升级全流程核心原理
    • 锁升级全流程总览
    • 3.1 第一阶段:无锁状态
      • 核心知识点:为什么调用identityHashCode会禁用偏向锁?
    • 3.2 第二阶段:偏向锁
      • 核心设计思想
      • JDK17特殊说明
      • 偏向锁的获取流程
      • 偏向锁的撤销机制
      • 批量重偏向与批量撤销
    • 3.3 第三阶段:轻量级锁
      • 核心设计思想
      • 轻量级锁的获取流程
      • 自适应自旋机制
      • 轻量级锁的释放流程
      • 适用场景
    • 3.4 第四阶段:重量级锁
      • 核心设计思想
      • 核心载体:ObjectMonitor
      • 重量级锁的获取流程
      • 性能开销说明
  • 四、锁升级全流程实战代码验证
    • 4.1 项目依赖配置(pom.xml)
    • 4.2 无锁状态与对象头验证
    • 4.3 偏向锁状态验证
    • 4.4 轻量级锁状态验证
    • 4.5 重量级锁状态验证
    • 4.6 identityHashCode导致偏向锁失效验证
  • 五、核心细节与易混淆点辨析
    • 5.1 锁升级是完全不可逆的吗?
    • 5.2 三种锁类型核心区别对比
    • 5.3 轻量级锁和自旋锁的关系?
    • 5.4 对象锁和类锁的核心区别?
    • 5.5 为什么wait/notify必须在synchronized同步块中调用?
    • 5.6 JDK17与JDK8中synchronized的核心区别?
  • 六、生产环境最佳实践
    • 6.1 锁粒度最小化原则
    • 6.2 避免在同步块中执行耗时操作
    • 6.3 合理选择锁对象
    • 6.4 JVM参数优化建议
    • 6.5 利用JIT的锁优化机制
    • 6.6 死锁规避规范
  • 七、高频面试题与标准答案
  • 结尾
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档