面试官追着问的synchronized底层:从对象头到锁升级,这篇讲透!

41 阅读9分钟

面试官追着问的synchronized底层:从对象头到锁升级,这篇讲透!

作为Java程序员,synchronized绝对是绕不开的核心知识点——日常开发中用它保证线程安全,面试时更是被高频追问。但很多人只停留在"会用"层面,遇到"为什么低并发快、高并发卡"、"锁升级到底是什么流程"这类问题就慌了神。

今天就带大家从底层到实战,彻底扒懂synchronized的核心逻辑,不仅能解决面试痛点,还能直接落地到项目优化中!

一、先看一个诡异现象:同样的synchronized,性能差100倍?

先看一段看似普通的代码,两个逻辑完全一致的synchronized方法,在不同并发场景下性能天差地别:

public class SyncTest {
    // 方法1:synchronized修饰普通方法
    public synchronized void method1() {
        int i = 0;
        i++;
    }
    
    // 方法2:和method1逻辑完全一致
    public synchronized void method2() {
        int i = 0;
        i++;
    }
}

// 测试代码
public class PerformanceTest {
    public static void main(String[] args) {
        SyncTest syncTest = new SyncTest();
        
        // 低并发:1个线程调用100万次
        long start1 = System.currentTimeMillis();
        new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                syncTest.method1();
            }
        }).start();
        long end1 = System.currentTimeMillis();
        System.out.println("低并发耗时:" + (end1 - start1) + "ms");
        
        // 高并发:1000个线程各调用1000次
        long start2 = System.currentTimeMillis();
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    syncTest.method2();
                }
            }).start();
        }
        long end2 = System.currentTimeMillis();
        System.out.println("高并发耗时:" + (end2 - start2) + "ms");
    }
}

运行结果(参考):

  • 低并发耗时:8ms
  • 高并发耗时:920ms

同样的代码,为什么并发强度不同,性能差距这么大?

答案很简单:synchronized不是"一刀切"的锁,而是JVM精心设计的"智能锁"——会根据并发情况自动升级锁状态,低并发用低成本的锁,高并发用高安全的锁。

要搞懂这个过程,必须先从synchronized的底层基石——对象头说起。

二、底层核心:对象头里藏着锁的秘密

在Java中,每个对象都有一个"对象头"(Object Header),它是实现锁的核心载体。以32位JVM为例,对象头由两部分组成:

  1. Mark Word(标记字) :占32位,存储锁状态、哈希码、GC年龄、偏向线程ID等关键信息(锁的核心);
  2. 类型指针:占32位,指向对象所属类的元数据(比如User对象指向User.class)。

其中Mark Word是重中之重,它的结构会随锁状态变化而变化,不同锁状态对应不同的Mark Word结构:

锁状态Mark Word结构(32位)核心说明
无锁哈希码(25位)+ GC年龄(4位)+ 无锁标记(3位)对象刚创建时的初始状态
偏向锁偏向线程ID(23位)+ 偏向时间戳(2位)+ GC年龄(4位)+ 偏向锁标记(3位)标记当前对象被哪个线程"偏爱"
轻量级锁指向栈中锁记录的指针(30位)+ 轻量级锁标记(2位)多个线程竞争但不激烈,用CAS实现
重量级锁指向互斥量的指针(30位)+ 重量级锁标记(2位)竞争激烈,依赖操作系统实现

简单说:锁升级的过程,本质就是Mark Word结构不断变化的过程。接下来我们一步步拆解完整升级流程。

三、锁升级全流程:无锁→偏向锁→轻量级锁→重量级锁

锁升级的核心逻辑:从低开销状态开始,随着并发竞争加剧逐步升级,且过程不可逆(一旦到重量级锁,就不会再降级)。

1. 无锁 → 偏向锁(低并发,无竞争)

  • 适用场景:只有一个线程多次获取同一个锁(比如单线程操作对象);

  • 升级过程

    • 线程A第一次获取锁时,JVM判断对象是无锁状态;
    • 通过CAS操作,将线程A的ID写入Mark Word,同时设置锁标记为"偏向锁";
    • 线程A再次获取锁时,只需比对Mark Word中的线程ID是否为自己,无需再次竞争,直接获取。
  • 核心优势:几乎无性能开销,第一次CAS,后续仅ID比对。

2. 偏向锁 → 轻量级锁(低并发,有竞争但不激烈)

  • 触发条件:有其他线程(线程B)尝试获取同一把锁,且持有锁的线程A仍在执行;

  • 升级过程

    • 线程B尝试获取锁,发现是偏向锁且偏向线程是A;

    • JVM暂停线程A,检查A是否还需要持有锁;

    • 若A已释放,重置为无锁状态,B通过CAS竞争为偏向锁;

    • 若A仍持有锁,升级为轻量级锁:

      1. A在自己的栈帧中创建"锁记录",存储Mark Word副本;
      2. A通过CAS将对象的Mark Word更新为指向自己锁记录的指针;
      3. B也创建锁记录,通过CAS自旋等待(不断重试),直到A释放锁。
  • 核心优势:用自旋代替线程阻塞(用户态操作,开销小),适合短时间竞争。

3. 轻量级锁 → 重量级锁(高并发,竞争激烈)

  • 触发条件:多个线程激烈竞争,自旋超时(比如自旋10次还没拿到锁),或自旋线程数超过CPU核心数的一半;

  • 升级过程

    • 线程B自旋超时,JVM判断竞争激烈,需升级为重量级锁;
    • 创建操作系统级别的互斥量(mutex),将Mark Word更新为指向互斥量的指针;
    • 线程B不再自旋,直接阻塞(切换到内核态),等待A释放锁;
    • A释放锁时,通过操作系统唤醒阻塞的B,B再次竞争。
  • 核心缺点:性能开销大,线程阻塞/唤醒需要内核态切换(开销是用户态的几十倍)。

锁升级完整流程图

bc9f0087e7240e3e892ef74c4b7618e6.png

四、企业级优化实战:让synchronized性能翻倍

理解了锁升级原理,优化的核心思路就很明确:尽量让锁停留在偏向锁/轻量级锁状态,避免升级为重量级锁。分享两个真实项目中常用的优化方案:

优化方案1:减小锁粒度(最常用)

问题场景

电商订单服务中,一个synchronized方法同时处理"创建订单"和"更新库存",高并发下锁升级为重量级锁,响应时间高达500ms。

错误代码:

public class OrderService {
    // 大锁包裹两个独立逻辑,竞争激烈
    public synchronized void processOrder(Order order) {
        createOrder(order); // 创建订单
        updateStock(order.getProductId(), order.getNum()); // 更新库存
    }
}
优化思路

将两个独立逻辑拆分为不同的锁,减小锁粒度,让两个逻辑可以并行执行,降低竞争强度。

优化后代码:

public class OrderService {
    // 为两个逻辑创建独立锁对象
    private final Object orderLock = new Object();
    private final Object stockLock = new Object();
    
    public void processOrder(Order order) {
        // 创建订单用orderLock
        synchronized (orderLock) {
            createOrder(order);
        }
        // 更新库存用stockLock
        synchronized (stockLock) {
            updateStock(order.getProductId(), order.getNum());
        }
    }
}
优化效果

锁竞争强度大幅降低,锁状态保持在轻量级锁,接口响应时间从500ms优化到80ms。

优化方案2:消除冗余锁

问题场景

synchronized修饰局部对象(不会被多线程共享),锁操作完全多余,浪费性能。

冗余代码:

// 局部对象不会被多线程共享,锁是多余的
public String processStr(String str) {
    synchronized (new Object()) {
        return str.toUpperCase();
    }
}
优化思路
  1. 直接删除冗余锁(推荐);
  2. 若无法修改代码,可通过JVM参数开启锁消除(-XX:+EliminateLocks,JDK8默认开启),JVM会自动识别并消除多余的锁。

优化后代码:

public String processStr(String str) {
    return str.toUpperCase();
}

五、面试必背:3道高频题+标准答案

1. 面试题1:synchronized的锁升级流程是什么?为什么要有偏向锁?

标准答案

  • 锁升级流程:无锁→偏向锁→轻量级锁→重量级锁(不可逆);

  • 各阶段逻辑:

    • 无锁→偏向锁:单线程多次获取锁时,CAS写入线程ID,后续直接比对,无竞争开销;
    • 偏向锁→轻量级锁:有线程竞争且持有锁线程未释放,升级为轻量级锁,用CAS+自旋竞争;
    • 轻量级锁→重量级锁:竞争激烈导致自旋超时,升级为重量级锁,线程阻塞;
  • 偏向锁的意义:大多数场景下,锁被同一个线程多次获取,偏向锁能最大限度减少锁竞争开销,提升低并发性能。

2. 面试题2:synchronized和ReentrantLock的底层差异?如何选型?

标准答案

  • 底层差异:

    • 实现方式:synchronized是JVM层面(对象头+Monitor),ReentrantLock是API层面(基于AQS);
    • 锁升级:synchronized支持自动升级,ReentrantLock需手动指定公平/非公平锁;
    • 功能:ReentrantLock支持中断、超时、条件变量,synchronized不支持;
    • 性能:低并发下synchronized更优(锁升级优化),高并发下两者接近,ReentrantLock更灵活;
  • 选型建议:

    • 简单场景(无特殊需求):用synchronized,代码简洁,JVM自动优化;
    • 复杂场景(需要中断、超时等):用ReentrantLock。

3. 面试题3:如何优化synchronized的性能?

标准答案

核心思路:避免锁升级为重量级锁,减少竞争开销;

具体方案:

  1. 减小锁粒度:拆分大锁为小锁,降低竞争强度;
  2. 锁粗化:避免循环内频繁加锁解锁,减少锁操作开销;
  3. 消除冗余锁:删除修饰局部对象的多余锁,或开启JVM锁消除;
  4. 合理使用偏向锁:低并发场景确保JVM开启偏向锁(JDK8默认开启)。

最后

synchronized的核心本质是"自适应的智能锁",JVM通过锁升级机制平衡了安全性和性能。掌握了对象头、锁升级流程和优化技巧,不仅能轻松应对面试,还能在项目中写出高性能的并发代码。

如果这篇文章对你有帮助,欢迎点赞+关注WX GZH【咖啡 Java 研习班】,后续会持续分享Java并发编程、JVM、Spring源码等硬核知识,带你从"会用"到"懂原理",一步步成为资深Java工程师!

最后留个思考题:为什么偏向锁和轻量级锁的性能比重量级锁好?欢迎在评论区留言讨论~