面试官追着问的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为例,对象头由两部分组成:
- Mark Word(标记字) :占32位,存储锁状态、哈希码、GC年龄、偏向线程ID等关键信息(锁的核心);
- 类型指针:占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仍持有锁,升级为轻量级锁:
- A在自己的栈帧中创建"锁记录",存储Mark Word副本;
- A通过CAS将对象的Mark Word更新为指向自己锁记录的指针;
- B也创建锁记录,通过CAS自旋等待(不断重试),直到A释放锁。
-
-
核心优势:用自旋代替线程阻塞(用户态操作,开销小),适合短时间竞争。
3. 轻量级锁 → 重量级锁(高并发,竞争激烈)
-
触发条件:多个线程激烈竞争,自旋超时(比如自旋10次还没拿到锁),或自旋线程数超过CPU核心数的一半;
-
升级过程:
- 线程B自旋超时,JVM判断竞争激烈,需升级为重量级锁;
- 创建操作系统级别的互斥量(mutex),将Mark Word更新为指向互斥量的指针;
- 线程B不再自旋,直接阻塞(切换到内核态),等待A释放锁;
- A释放锁时,通过操作系统唤醒阻塞的B,B再次竞争。
-
核心缺点:性能开销大,线程阻塞/唤醒需要内核态切换(开销是用户态的几十倍)。
锁升级完整流程图
四、企业级优化实战:让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();
}
}
优化思路
- 直接删除冗余锁(推荐);
- 若无法修改代码,可通过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的性能?
标准答案:
核心思路:避免锁升级为重量级锁,减少竞争开销;
具体方案:
- 减小锁粒度:拆分大锁为小锁,降低竞争强度;
- 锁粗化:避免循环内频繁加锁解锁,减少锁操作开销;
- 消除冗余锁:删除修饰局部对象的多余锁,或开启JVM锁消除;
- 合理使用偏向锁:低并发场景确保JVM开启偏向锁(JDK8默认开启)。
最后
synchronized的核心本质是"自适应的智能锁",JVM通过锁升级机制平衡了安全性和性能。掌握了对象头、锁升级流程和优化技巧,不仅能轻松应对面试,还能在项目中写出高性能的并发代码。
如果这篇文章对你有帮助,欢迎点赞+关注WX GZH【咖啡 Java 研习班】,后续会持续分享Java并发编程、JVM、Spring源码等硬核知识,带你从"会用"到"懂原理",一步步成为资深Java工程师!
最后留个思考题:为什么偏向锁和轻量级锁的性能比重量级锁好?欢迎在评论区留言讨论~