synchronized锁优化与升级机制
synchronized锁优化与升级机制
深入理解JVM对synchronized的优化:从无锁到偏向锁、轻量级锁,再到重量级锁的完整升级过程。
为什么需要锁优化
传统synchronized的问题
在JDK 1.6之前,synchronized是重量级锁:
传统synchronized的缺点:
├─ 总是使用Monitor(重量级锁)
├─ 涉及用户态↔内核态切换
├─ 需要操作系统调度
└─ 性能开销大
性能对比(JDK 1.5):
// 测试代码
int count = 0;// 无锁
for (int i = 0; i < 1000000; i++) {count++;
}
// 耗时:~2ms// synchronized(总是重量级)
synchronized (lock) {for (int i = 0; i < 1000000; i++) {count++;}
}
// 耗时:~200ms(慢100倍!)
JDK 1.6的优化
JDK 1.6对synchronized进行了大量优化,引入了锁升级机制:
锁升级路径:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁优化效果:
├─ 大部分情况下不需要重量级锁
├─ 性能提升10-100倍
└─ synchronized变得高效
优化后的性能(JDK 1.8+):
// synchronized(有锁优化)
synchronized (lock) {for (int i = 0; i < 1000000; i++) {count++;}
}
// 耗时:~3-5ms(接近无锁!)
锁的四种状态
状态概览
Java对象头的Mark Word存储锁状态信息:
┌─────────────────────────────────────┐
│ 锁状态(64位JVM) │
├─────────────────────────────────────┤
│ 无锁(Normal) │
│ ├─ 锁标志位:01 │
│ ├─ 偏向锁标志:0 │
│ └─ 存储:hashCode、GC年龄 │
├─────────────────────────────────────┤
│ 偏向锁(Biased) │
│ ├─ 锁标志位:01 │
│ ├─ 偏向锁标志:1 │
│ └─ 存储:线程ID、epoch、GC年龄 │
├─────────────────────────────────────┤
│ 轻量级锁(Lightweight Locked) │
│ ├─ 锁标志位:00 │
│ └─ 存储:指向栈中Lock Record的指针 │
├─────────────────────────────────────┤
│ 重量级锁(Heavyweight Locked) │
│ ├─ 锁标志位:10 │
│ └─ 存储:指向Monitor的指针 │
└─────────────────────────────────────┘
Mark Word结构详解
64位JVM的Mark Word
无锁状态:
┌─────────┬──────────┬────┬────┬───┬──┐
│unused(25)│hashCode(31)│un-│age │0│01│
│ │ │used│(4) │ │ │
└─────────┴──────────┴────┴────┴───┴──┘↑identity hashCode偏向锁状态:
┌───────────┬────────┬────┬────┬───┬──┐
│Thread ID │epoch(2)│un- │age │1│01│
│ (54) │ │used│(4) │ │ │
└───────────┴────────┴────┴────┴───┴──┘↑偏向的线程ID轻量级锁:
┌──────────────────────────────────┬──┐
│指向栈中Lock Record的指针(62bit) │00│
└──────────────────────────────────┴──┘重量级锁:
┌──────────────────────────────────┬──┐
│指向Monitor对象的指针(62bit) │10│
└──────────────────────────────────┴──┘GC标记:
┌──────────────────────────────────┬──┐
│ 空 │11│
└──────────────────────────────────┴──┘
锁状态对比
| 锁状态 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 无锁 | 性能最好 | 无同步保证 | 无竞争 |
| 偏向锁 | 加锁解锁无需CAS | 有竞争需要撤销 | 一个线程访问同步块 |
| 轻量级锁 | 竞争不激烈时性能好 | 自旋消耗CPU | 线程交替执行同步块 |
| 重量级锁 | 不消耗CPU | 线程阻塞,上下文切换 | 竞争激烈 |
偏向锁详解
基本概念
偏向锁(Biased Locking):锁偏向于第一个获取它的线程。
核心思想:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。
工作原理
偏向锁的获取
线程第一次进入同步块:步骤1:检查Mark Word├─ 如果是无锁状态(biased_lock=0)└─ 进入步骤2步骤2:使用CAS将Mark Word替换为偏向锁├─ Thread ID = 当前线程ID├─ biased_lock = 1├─ lock = 01└─ CAS成功 → 获得偏向锁步骤3:以后该线程再进入├─ 检查Thread ID == 当前线程├─ 如果相等 → 直接进入(无需CAS!)└─ 如果不相等 → 偏向锁撤销
图示:
初始状态(无锁):
Object Header:
┌─────────────────────────────┐
│ hashCode | age | 0 | 01 │
└─────────────────────────────┘Thread-1第一次加锁:
┌─────────────────────────────┐
│ Thread-1 ID | age | 1 | 01 │ ← CAS设置
└─────────────────────────────┘Thread-1再次加锁:
┌─────────────────────────────┐
│ Thread-1 ID | age | 1 | 01 │ ← 直接进入,无CAS!
└─────────────────────────────┘
代码示例
public class BiasedLockDemo {private static Object obj = new Object();public static void main(String[] args) throws InterruptedException {// JVM启动后需要等待4秒才会启用偏向锁Thread.sleep(5000);Thread t1 = new Thread(() -> {synchronized (obj) {System.out.println("第一次加锁");// 第一次:CAS设置偏向锁}synchronized (obj) {System.out.println("第二次加锁");// 第二次:检查Thread ID,直接进入}synchronized (obj) {System.out.println("第三次加锁");// 第三次:检查Thread ID,直接进入}});t1.start();t1.join();// 使用JOL查看对象头System.out.println(ClassLayout.parseInstance(obj).toPrintable());}
}
对象头变化:
第一次加锁前:
00000000 00000000 00000000 00000000 ... 00000001 (无锁 01)第一次加锁后:
00000000 00000000 00000111 10110101 ... 00000101 (偏向锁 101)↑Thread ID
偏向锁的撤销
当其他线程尝试获取偏向锁时,需要撤销偏向锁:
偏向锁撤销流程:1. 暂停拥有偏向锁的线程(STW)└─ 到达安全点2. 检查持有偏向锁的线程状态├─ 如果线程不再活跃 → 直接撤销└─ 如果线程还活跃 → 继续步骤33. 检查线程是否还在同步块中├─ 如果不在 → 撤销偏向锁,改为无锁└─ 如果在 → 升级为轻量级锁4. 恢复线程执行
示例:
public class BiasedLockRevoke {private static Object obj = new Object();public static void main(String[] args) throws InterruptedException {Thread.sleep(5000);// Thread-1持有偏向锁Thread t1 = new Thread(() -> {synchronized (obj) {System.out.println("t1: 获得偏向锁");try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();Thread.sleep(100);// Thread-2尝试获取锁 → 偏向锁撤销Thread t2 = new Thread(() -> {synchronized (obj) {System.out.println("t2: 偏向锁被撤销,升级为轻量级锁");}});t2.start();}
}
批量重偏向和批量撤销
当撤销次数达到阈值时,JVM会进行批量操作:
批量重偏向(Bulk Rebias):
├─ 触发条件:同一类型对象撤销次数达到20次
├─ 操作:epoch++,使旧的偏向失效
└─ 效果:新的偏向可以快速建立批量撤销(Bulk Revoke):
├─ 触发条件:同一类型对象撤销次数达到40次
├─ 操作:关闭该类的偏向锁
└─ 效果:该类的对象直接使用轻量级锁
偏向锁的关闭
# 关闭偏向锁
-XX:-UseBiasedLocking# 延迟启用偏向锁(默认4秒)
-XX:BiasedLockingStartupDelay=0# 查看偏向锁统计
-XX:+PrintBiasedLockingStatistics
轻量级锁详解
基本概念
轻量级锁(Lightweight Locking):通过CAS操作避免使用重量级锁的互斥量。
核心思想:在没有多线程竞争的情况下,通过CAS减少重量级锁的使用。
工作原理
加锁过程
线程尝试获取轻量级锁:步骤1:在栈帧中创建Lock Record├─ 拷贝对象头的Mark Word到Lock Record└─ Lock Record中有一个owner指针指向锁对象步骤2:使用CAS尝试将对象头的Mark Word替换为指向Lock Record的指针├─ CAS成功 → 获得锁└─ CAS失败 → 步骤3步骤3:检查是否是重入├─ 如果Mark Word指向当前线程的栈帧│ → 重入,添加一个Lock Record(Displaced Mark Word为null)└─ 否则 → 锁竞争,自旋步骤4:自旋一定次数后└─ 升级为重量级锁
图示:
加锁前:
Object: Thread Stack:
┌─────────────┐
│ Mark Word │
│ hashCode|01 │
└─────────────┘加锁中:
Object: Thread Stack:
┌─────────────┐ ┌──────────────────┐
│ Mark Word │ │ Lock Record │
│ ─────────────────────────>│ ├─ Displaced MW │
│ ptr to LR|00│ │ │ (hashCode) │
└─────────────┘ │ └─ owner → Object│└──────────────────┘重入:
Object: Thread Stack:
┌─────────────┐ ┌──────────────────┐
│ Mark Word │ │ Lock Record (2) │
│ ─────────────────────────>│ ├─ Displaced MW │
│ ptr to LR|00│ │ │ (null) │← 重入标记
└─────────────┘ │ └─ owner → Object│├──────────────────┤│ Lock Record (1) ││ ├─ Displaced MW ││ │ (hashCode) ││ └─ owner → Object│└──────────────────┘
解锁过程
解锁过程:步骤1:取出Lock Record中的Displaced Mark Word步骤2:使用CAS将Displaced Mark Word替换回对象头├─ CAS成功 → 解锁成功└─ CAS失败 → 说明有竞争,锁已升级为重量级锁步骤3:如果是重量级锁└─ 按照重量级锁的方式释放(释放Monitor)
代码示例
public class LightweightLockDemo {private static Object obj = new Object();public static void main(String[] args) throws InterruptedException {// 关闭偏向锁,直接使用轻量级锁// -XX:-UseBiasedLockingThread t1 = new Thread(() -> {synchronized (obj) {System.out.println("t1: 获得轻量级锁");// CAS设置Lock Record指针}});t1.start();t1.join();// 使用JOL查看对象头System.out.println(ClassLayout.parseInstance(obj).toPrintable());}
}
自旋优化
轻量级锁使用**自旋(Spinning)**来避免线程阻塞:
// 自旋等待锁
int spinCount = 0;
while (!tryAcquireLock()) {spinCount++;if (spinCount > MAX_SPIN_COUNT) {// 自旋次数过多,升级为重量级锁inflateToHeavyweight();break;}// 短暂等待(自旋)// CPU会一直执行这个循环
}
自旋的优缺点:
优点:
├─ 避免线程阻塞和唤醒
├─ 减少上下文切换
└─ 适合锁持有时间短的场景缺点:
├─ 消耗CPU资源
├─ 如果锁持有时间长,浪费CPU
└─ 自旋次数难以确定
自适应自旋:
JVM会根据历史记录动态调整自旋次数:
自适应策略:
├─ 如果上次自旋成功 → 增加自旋次数
├─ 如果上次自旋失败 → 减少自旋次数
└─ 如果某个锁自旋很少成功 → 直接跳过自旋
重量级锁详解
基本概念
重量级锁(Heavyweight Lock):使用操作系统的互斥量(Mutex)实现的锁。
特点:
- 🔒 使用Monitor对象
- 🔒 线程会被阻塞(BLOCKED状态)
- 🔒 涉及用户态↔内核态切换
- 🔒 性能开销最大
升级时机
升级为重量级锁的条件:1. 轻量级锁自旋失败└─ 自旋次数超过阈值2. 轻量级锁CAS失败└─ 有线程正在持有锁3. wait/notify调用└─ 这些操作需要Monitor
锁膨胀过程
锁膨胀(Lock Inflation):步骤1:创建Monitor对象步骤2:将Mark Word设置为指向Monitor的指针└─ 锁标志位改为10步骤3:将当前持有轻量级锁的线程设置为Monitor的Owner步骤4:竞争失败的线程进入Monitor的EntryList步骤5:后续操作按照Monitor机制进行
图示:
轻量级锁:
Object: Thread Stack:
┌──────────────┐ ┌─────────────┐
│ ptr to LR|00 │────────> │ Lock Record │
└──────────────┘ └─────────────┘膨胀为重量级锁:
Object: Monitor:
┌──────────────┐ ┌─────────────────┐
│ ptr to Mon|10│────────> │ Owner: Thread-1 │
└──────────────┘ │ EntryList: ││ ├─ Thread-2 ││ └─ Thread-3 ││ WaitSet: [] │└─────────────────┘
锁升级过程
完整升级路径
┌──────────────────────────────────────────────────────────────┐
│ synchronized锁升级完整流程 │
└──────────────────────────────────────────────────────────────┘┌─────────────┐│ 无锁状态 ││MarkWord: ││ hashCode等 │└──────┬──────┘│第一个线程synchronized(obj)│▼┌────────────────────┐│ JVM检查是否可偏向?│└──┬──────────┬──────┘YES NO│ │▼ └───────────────┐┌─────────────┐ ││ 偏向锁状态 │ ││MarkWord: │ ││ Thread ID │ │└──────┬──────┘ ││ │其他线程synchronized(obj) ││ │▼ │┌───────────────────────┐ ││ 偏向线程还在执行同步块?│ │└──┬───────────┬────────┘ │YES NO ││ │ ││ ▼ ││ ┌──────────────┐ ││ │ 偏向锁撤销 │ ││ │(到安全点) │ ││ └──────┬───────┘ ││ │ ││ └───────────┬───────────┘▼ ▼┌─────────┐ ┌──────────────┐│ 升级为 │ │ 升级为 ││重量级锁 │ │ 轻量级锁 │└─────────┘ │ MarkWord: ││ Lock Record ││ 指针 │└──────┬────────┘│多线程竞争,CAS自旋│▼┌────────────────────┐│ CAS自旋获取锁成功? │└──┬──────────┬──────┘YES NO│ │▼ ▼┌────────┐ ┌─────────────┐│轻量级锁│ │ 自旋一定次数 ││运行 │ │ 仍失败? │└────────┘ └──┬──────────┘│ YES▼┌─────────────────┐│ 膨胀为重量级锁 ││ MarkWord: ││ Monitor指针 │└─────────────────┘【关键点】:
✅ 锁只能升级,不能降级(JDK 15之前)
✅ 偏向锁撤销需要到达安全点(STW)
✅ 轻量级锁使用CAS+自旋
✅ 重量级锁使用操作系统互斥量
各阶段Mark Word变化
阶段1:无锁 → 偏向锁
┌────────────────────────────┐ ┌────────────────────────────┐
│ hashCode | age | 0 | 01 │ → │ ThreadID | epoch|age|1|01 │
└────────────────────────────┘ └────────────────────────────┘hashCode被覆盖 记录偏向线程ID阶段2:偏向锁 → 轻量级锁
┌────────────────────────────┐ ┌────────────────────────────┐
│ ThreadID | epoch|age|1|01 │ → │ Lock Record指针 | 00 │
└────────────────────────────┘ └────────────────────────────┘撤销偏向,CAS替换 指向线程栈的Lock Record阶段3:轻量级锁 → 重量级锁
┌────────────────────────────┐ ┌────────────────────────────┐
│ Lock Record指针 | 00 │ → │ Monitor指针 | 10 │
└────────────────────────────┘ └────────────────────────────┘自旋失败,膨胀 指向Monitor对象
详细示例
import org.openjdk.jol.info.ClassLayout;public class LockUpgradeDemo {public static void main(String[] args) throws InterruptedException {Object obj = new Object();// 等待偏向锁启用Thread.sleep(5000);System.out.println("===== 初始状态(无锁) =====");System.out.println(ClassLayout.parseInstance(obj).toPrintable());// 阶段1:偏向锁Thread t1 = new Thread(() -> {synchronized (obj) {System.out.println("\n===== t1获得偏向锁 =====");System.out.println(ClassLayout.parseInstance(obj).toPrintable());}});t1.start();t1.join();System.out.println("\n===== t1释放后(仍是偏向锁) =====");System.out.println(ClassLayout.parseInstance(obj).toPrintable());// 阶段2:轻量级锁Thread t2 = new Thread(() -> {synchronized (obj) {System.out.println("\n===== t2获得锁(轻量级) =====");System.out.println(ClassLayout.parseInstance(obj).toPrintable());}});t2.start();t2.join();// 阶段3:重量级锁Thread t3 = new Thread(() -> {synchronized (obj) {System.out.println("\n===== t3持有锁(重量级) =====");System.out.println(ClassLayout.parseInstance(obj).toPrintable());try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}});Thread t4 = new Thread(() -> {synchronized (obj) {System.out.println("\n===== t4获得锁 =====");}});t3.start();Thread.sleep(100);t4.start();Thread.sleep(500);System.out.println("\n===== 竞争中(重量级锁) =====");System.out.println(ClassLayout.parseInstance(obj).toPrintable());t3.join();t4.join();}
}
输出分析:
===== 初始状态(无锁) =====
...00000001 (锁标志位 01,无偏向)===== t1获得偏向锁 =====
...00000101 (锁标志位 01,有偏向)
Thread ID: 0x00007f8a1c001000===== t1释放后(仍是偏向锁) =====
...00000101 (偏向锁不会主动撤销)
Thread ID: 0x00007f8a1c001000===== t2获得锁(轻量级) =====
...00000000 (锁标志位 00,轻量级锁)
Lock Record Pointer: 0x00007f8a1c123456===== 竞争中(重量级锁) =====
...00000010 (锁标志位 10,重量级锁)
Monitor Pointer: 0x00007f8a1c789abc
锁降级
注意:Java的锁通常不会降级!
锁升级:✅ 支持
无锁 → 偏向锁 → 轻量级锁 → 重量级锁锁降级:❌ 通常不支持
重量级锁 → 轻量级锁 → 偏向锁 → 无锁
原因:
- 降级实现复杂
- 降级判断开销大
- 大部分场景不需要降级
特殊情况:
- GC时可能清理无用的Monitor
- 偏向锁epoch机制可以理解为一种"降级"
锁优化技术
1. 锁消除(Lock Elimination)
JIT编译器会消除不可能存在竞争的锁:
public void method() {// 局部变量,不可能被其他线程访问Object lock = new Object();synchronized (lock) { // ← 这个锁会被消除// ...}
}// JIT优化后等价于:
public void method() {// 直接执行,无锁// ...
}
触发条件:
- 逃逸分析证明对象不会逃逸
- 锁对象只在当前线程可见
2. 锁粗化(Lock Coarsening)
将多个连续的加锁操作合并为一个:
// 优化前:频繁加锁解锁
for (int i = 0; i < 1000; i++) {synchronized (lock) {list.add(i);}
}// JIT优化后:锁粗化
synchronized (lock) {for (int i = 0; i < 1000; i++) {list.add(i);}
}
3. 自适应自旋
动态调整自旋次数:
历史记录:
├─ 上次自旋成功 → 增加自旋次数(最多10次)
├─ 上次自旋失败 → 减少自旋次数
└─ 连续失败 → 跳过自旋,直接阻塞
4. 偏向锁优化
优化策略:
├─ 延迟启用(默认4秒后)
├─ 批量重偏向(epoch机制)
└─ 批量撤销(类级别关闭)
🎯 知识点总结
锁状态对比
| 锁状态 | Mark Word | 获取方式 | 释放方式 | 适用场景 |
|---|---|---|---|---|
| 无锁 | hashCode|01 | - | - | 无竞争 |
| 偏向锁 | Thread ID|101 | CAS一次 | 撤销时处理 | 单线程反复进入 |
| 轻量级锁 | Lock Record|00 | CAS | CAS | 交替执行 |
| 重量级锁 | Monitor|10 | Monitor | Monitor | 竞争激烈 |
锁升级条件
无锁 → 偏向锁:
└─ 第一个线程进入同步块偏向锁 → 轻量级锁:
└─ 其他线程尝试获取锁轻量级锁 → 重量级锁:
├─ 自旋次数超过阈值
├─ 自旋线程数超过CPU核心数的一半
└─ 调用wait/notify
优化建议
- ✅ 减少锁的持有时间
- ✅ 减小锁的粒度
- ✅ 使用读写锁代替独占锁
- ✅ 锁分离(如ReadWriteLock)
- ✅ 锁分段(如ConcurrentHashMap)
💡 常见面试题
Q1:synchronized的锁有哪几种状态?
答:4种状态:无锁、偏向锁、轻量级锁、重量级锁。随着竞争激烈程度,锁会从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁。这个过程是单向的,通常不会降级。
Q2:什么是偏向锁?
答:偏向锁是一种针对加锁操作的优化手段。假设某个锁大部分时间都是被同一个线程获取,那么这个锁就"偏向"这个线程。偏向锁使用CAS一次性将线程ID记录在对象头中,之后该线程再次进入时只需检查线程ID,无需CAS,大大提高了性能。
Q3:轻量级锁是如何工作的?
答:轻量级锁通过CAS操作避免使用互斥量。加锁时,在栈帧中创建Lock Record,使用CAS将对象头替换为指向Lock Record的指针。如果CAS失败,会进行自旋等待。自旋一定次数后仍失败,则升级为重量级锁。
Q4:为什么需要自旋?
答:因为线程阻塞和唤醒需要从用户态切换到内核态,开销很大。如果锁很快就会被释放,那么自旋等待比阻塞更高效。自旋适合锁持有时间短、竞争不激烈的场景。
Q5:synchronized在JDK 1.6前后有什么变化?
答:JDK 1.6之前,synchronized总是使用重量级锁,性能较差。JDK 1.6引入了偏向锁、轻量级锁、自旋锁、锁消除、锁粗化等优化,使得synchronized在大部分情况下性能接近甚至超过ReentrantLock,成为高效的同步机制。
