偏向锁
、轻量级锁
、自旋锁
是 Java 中 synchronized 锁在 JVM 层面的优化机制,它们属于 锁升级(Lock Escalation) 的一部分,目的是在不同竞争场景下提升性能。
下面我将从 概念、原理、触发条件、使用方式、性能对比 等维度,系统性地为你详细介绍这三种锁。
一、背景:为什么需要这些优化?
在早期 JDK 版本中,synchronized
是一个“重量级锁”,依赖操作系统底层的互斥量(Mutex),每次加锁/解锁都要进行用户态和内核态的切换,开销大。
从 JDK 1.6 开始,HotSpot JVM 对 synchronized
进行了深度优化,引入了:
偏向锁 → 轻量级锁 → 重量级锁 的升级机制
目标是:在无竞争或低竞争场景下,尽量避免进入操作系统级别的重量级锁,提升性能。
二、三种锁的对比概览
锁类型 | 适用场景 | 是否阻塞 | 实现方式 | 性能开销 |
---|---|---|---|---|
偏向锁 | 只有一个线程访问 | ❌ 不阻塞 | CAS 修改对象头 | ⚡️ 极低 |
轻量级锁 | 少量线程交替执行 | ❌ 自旋等待 | 栈帧 + CAS | ⚡️ 低 |
重量级锁 | 多线程激烈竞争 | ✅ 阻塞 | 操作系统 Mutex | ⚠️ 高 |
自旋锁(配合轻量级锁) | 竞争时间短 | ❌ 循环等待 | CPU 空转 | ⚡️ 低(短时间) |
⚠️ 注意:自旋锁不是独立的锁状态,它是轻量级锁的一种“等待策略”。
三、详细讲解
✅ 1. 偏向锁(Biased Locking)
🎯 概念:
偏向锁的核心思想是:“这个锁只会被一个线程使用”,所以它“偏向”于第一个获取它的线程。之后该线程再次进入时,不需要任何同步操作。
🧠 原理:
- 当一个线程第一次获取锁时,JVM 会使用 CAS 将线程 ID 记录在对象头(Mark Word)中。
- 以后该线程再进入同步块时,只需要检查对象头中的线程 ID 是否是自己,如果是,直接执行,无需加锁。
- 只有当其他线程尝试竞争时,才会升级为轻量级锁。
✅ 优点:
- 减少无竞争场景下的同步开销;
- 适合“一个线程多次进入同步块”的场景(如遍历、单线程初始化)。
⚠️ 缺点:
- 如果系统中大多数锁都有竞争,开启偏向锁反而增加额外的 CAS 操作;
- JDK 15 开始,默认禁用偏向锁(可通过参数开启)。
🔧 开启/关闭(JVM 参数):
-XX:+UseBiasedLocking # 开启偏向锁(JDK 15 前默认开启)
-XX:-UseBiasedLocking # 关闭偏向锁
-XX:BiasedLockingStartupDelay=0 # 偏向锁延迟时间(默认 4 秒)
📌 示例场景:
java深色版本public class Counter {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
}
如果只有一个线程调用 increment()
,偏向锁会一直持有,性能接近无锁。
✅ 2. 轻量级锁(Lightweight Locking)
🎯 概念:
当有第二个线程尝试获取锁时,偏向锁升级为轻量级锁。它通过 CAS 操作 + 栈帧锁记录 实现,避免进入操作系统级别的重量级锁。
🧠 原理:
- 线程在自己的栈帧中创建一个 锁记录(Lock Record);
- JVM 使用 CAS 将对象头的 Mark Word 复制到锁记录中;
- 再用 CAS 将对象头指向锁记录(表示加锁成功);
- 如果 CAS 失败,说明有竞争,可能升级为重量级锁。
✅ 优点:
- 避免线程阻塞和唤醒的开销;
- 适合线程交替执行的场景(如生产者-消费者)。
⚠️ 缺点:
- 如果竞争激烈,CAS 多次失败,会升级为重量级锁;
- 占用额外的栈空间。
📌 触发条件:
- 偏向锁被打破(有线程竞争);
- 竞争不激烈,线程交替获取锁。
✅ 3. 自旋锁(Spinning Lock)
🎯 概念:
自旋锁不是一种独立的锁状态,而是 轻量级锁的一种“等待策略”。当线程尝试获取锁失败时,它不会立即阻塞,而是在 CPU 上空转(循环)一段时间,等待锁被释放。
🧠 原理:
java深色版本while (!tryLock()) {
// 空循环,等待锁释放
// 可能加上 Thread.yield() 让出 CPU 时间片
}
✅ 优点:
- 避免线程阻塞/唤醒的开销(上下文切换很贵);
- 如果锁持有时间很短(如几毫秒),自旋等待比阻塞更快。
⚠️ 缺点:
- 浪费 CPU 资源(空转);
- 如果锁持有时间长,自旋会白白消耗 CPU。
🔧 自旋次数控制(JVM 参数):
bash深色版本-XX:PreBlockSpin=10 # 默认自旋 10 次
-XX:+UseSpinning # 开启自旋(JDK 1.7 前)
JDK 1.7+ 之后,自旋策略由 JVM 自适应调整(Adaptive Spinning),不再需要手动设置。
📌 适用场景:
- 多核 CPU;
- 锁持有时间极短;
- 线程竞争不激烈。
四、锁升级过程(完整流程)
text深色版本 ┌─────────────┐
│ 无锁状态 │
└─────────────┘
↓
第一次由线程 A 获取
↓
┌─────────────┐
│ 偏向锁 │ ←──── 线程 A 再次进入:无开销
└─────────────┘
↓
线程 B 尝试获取(竞争)
↓
┌─────────────┐
│ 轻量级锁 │ ←──── 线程 A/B 交替执行,CAS + 自旋
└─────────────┘
↓
竞争激烈,自旋失败多次
↓
┌─────────────┐
│ 重量级锁 │ ←──── 进入操作系统 Mutex,线程阻塞
└─────────────┘
🔁 注意:锁可以升级,但不会降级(除了重量级锁降级为偏向锁在某些 GC 后可能发生)。
五、如何观察锁状态?
虽然开发者不需要手动控制锁升级(JVM 自动完成),但我们可以通过一些工具观察:
1. 使用 jcmd
查看锁信息
bash深色版本jcmd <pid> Thread.print
2. 使用 JOL(Java Object Layout)工具查看对象头
java深色版本<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
java深色版本import org.openjdk.jol.info.ClassLayout;
public class LockTest {
static Object obj = new Object();
public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
输出中可以看到对象头的 Mark Word,判断当前锁状态(无锁、偏向、轻量级、重量级)。
六、使用建议与最佳实践
✅ 推荐做法:
优先使用 synchronized
:JVM 优化充分,代码简洁;- 不要手动干预锁升级:这是 JVM 的职责;
- 减少锁的粒度:只锁必要的代码段;
- 避免在锁中做耗时操作(如 IO、网络请求);
高并发场景可考虑 ReentrantLock
,支持更多控制。
❌ 不要这样做:
java深色版本synchronized (new Object()) {
// 每次都是新对象,锁无效!
}
七、总结:三种锁的本质
锁 | 核心思想 | 适用场景 | 是否需要操作系统介入 |
---|---|---|---|
偏向锁 | “这个锁只归我” | 单线程频繁访问 | ❌ |
轻量级锁 | “我们轮流用,不打架” | 少量线程交替执行 | ❌ |
自旋锁 | “我等你一下,别阻塞” | 锁持有时间极短 | ❌ |
重量级锁 | “排队等,别吵” | 激烈竞争 | ✅ |
✅ 一句话总结:
“偏向锁偏爱独占,轻量级锁靠 CAS 争; 自旋锁空转等释放,重量级锁进内核; JVM 自动来升级,多线程性能稳如金。”
掌握这些锁的原理,你就能理解为什么现代 Java 中 synchronized
已经不再“重量级”,而是高性能的同步首选。
> 轻量级锁的核心实现机制
🧱 前提知识:对象头(Mark Word)
在 HotSpot JVM 中,每个 Java 对象在内存中都有一个 对象头(Object Header),其中最重要的部分是 Mark Word,它记录了对象的运行时元数据,比如:
- 哈希码
- GC 分代年龄
- 锁状态(无锁、偏向锁、轻量级锁、重量级锁)
- 指向锁记录或 Monitor 的指针
🔍 你可以把 Mark Word 看作是一个“多功能寄存器”,根据锁的状态,它的含义会变化。
🔍 逐句解析
第1句:“线程在自己的栈帧中创建一个锁记录(Lock Record)”
✅ 含义:
当一个线程要获取轻量级锁时,JVM 会在该线程的 Java 栈帧(Stack Frame) 中分配一块空间,叫做 锁记录(Lock Record)。
🎯 目的:
这个锁记录用来保存对象原本的 Mark Word,以便将来解锁时恢复。
🖼️ 示意图:
text深色版本线程栈(Thread Stack)
┌─────────────────┐
│ 栈帧(Frame) │
│ │
│ ┌───────────┐ │
│ │ Lock Record │←┼── 指向堆中的对象
│ │ - displaced│
│ │ Mark Word│ ← 存放原 Mark Word 的副本
│ └───────────┘ │
└─────────────────┘
🔑 这个 displaced Mark Word 就是“被替换出去的 Mark Word”。
第2句:“JVM 使用 CAS 将对象头的 Mark Word 复制到锁记录中”
✅ 含义:
JVM 先尝试用 CAS(Compare and Swap) 操作,把对象头中的 Mark Word 复制到 刚才创建的锁记录中。
🤔 为什么?
因为接下来我们要修改对象头,必须先备份原始数据,否则就丢了。
⚠️ 注意:
- 这一步只是“读取 + 备份”,还没有加锁;
- 使用 CAS 是为了保证在并发环境下,这个读取是安全的(避免其他线程同时也在操作)。
第3句:“再用 CAS 将对象头指向锁记录(表示加锁成功)”
✅ 含义:
这是真正的“加锁”操作!
JVM 再次使用 CAS 操作,把对象头中的 Mark Word 修改为指向当前线程栈中锁记录的指针。
🖼️ 加锁后的状态:
text深色版本堆(Heap)
┌─────────────┐
│ Object │
│ ┌─────────┐ │
│ │ Mark Word ├─→ 指向栈中的 Lock Record
│ └─────────┘ │
└─────────────┘
线程栈
┌─────────────────┐
│ 栈帧(Frame) │
│ │
│ ┌───────────┐ │
│ │ Lock Record │←┼── 被对象头指向
│ │ - displaced│
│ │ Mark Word│ ← 存放原 Mark Word
│ └───────────┘ │
└─────────────────┘
✅ 成功意味着:
- 当前线程成功获取了轻量级锁;
- 对象头现在指向了这个线程的锁记录,别人就知道“这把锁被占用了”。
第4句:“如果 CAS 失败,说明有竞争,可能升级为重量级锁”
❌ CAS 失败的原因:
- 另一个线程已经成功修改了对象头(即已经加锁);
- 此时存在多线程竞争。
🔄 接下来会发生什么?
- 当前线程发现 CAS 失败,说明轻量级锁拿不到;
- JVM 会尝试 自旋(Spinning):循环几次,看看持有锁的线程是否很快释放;
- 如果自旋一定次数后仍失败(默认 10 次,JDK 1.7+ 自适应):
🖼️ 重量级锁状态:
text深色版本堆
┌─────────────┐
│ Object │
│ ┌─────────┐ │
│ │ Mark Word ├─→ 指向 Monitor
│ └─────────┘ │
└─────────────┘
┌─────────────┐
│ Monitor │ ← 操作系统级别的互斥量
│ ┌─────────┐ │
│ │ Owner │ ← 当前持有锁的线程
│ │ EntryQ │ ← 阻塞等待的线程队列
│ └─────────┘ │
└─────────────┘
🧩 总结:轻量级锁的工作流程(图解)
text深色版本步骤1:线程准备加锁
↓
创建 Lock Record(在栈中)
↓
步骤2:CAS 备份 Mark Word 到 Lock Record
↓
步骤3:CAS 修改对象头 → 指向 Lock Record
↓
成功? ——是——→ 加锁成功(轻量级锁)
↓否
失败?
↓是
开始自旋等待...
↓
自旋多次仍失败?
↓是
升级为重量级锁(Monitor)
✅ 举个生活化的比喻
想象你在图书馆抢一个自习座位:
- 无锁:座位空着。
- 偏向锁:你每天固定来,管理员说:“这位置给你留着,你直接坐。”
- 轻量级锁:你来了,发现没人,但不确定会不会有人来抢。你先放个书包占座(CAS 修改对象头),然后坐下学习。
- 自旋锁:另一个人来了,看到你书包在,他不走,就在旁边转圈等(自旋),看你什么时候走。
- 重量级锁:等太久,他去登记处排队(阻塞),等你走了管理员叫他。
💡 关键点总结
概念 | 作用 |
---|---|
Lock Record | 在栈中保存原始 Mark Word,用于解锁恢复 |
CAS | 无锁方式修改对象头,保证原子性 |
对象头指向 Lock Record | 表示“我被这个线程锁住了” |
CAS 失败 → 自旋 → 升级 | 竞争激烈时自动升级为重量级锁 |
✅ 最后一句话:
“轻量级锁不靠操作系统,靠 CAS + 栈记录; 备份 Mark Word,改头换面把锁占; 竞争一来就自旋,太久就进 Monitor 队列。”
你现在理解的,正是 synchronized
高性能的秘密所在!👏
✅ 一、轻量级锁的释放过程(解锁)
当线程执行完 synchronized
代码块后,JVM 会执行 轻量级锁的释放流程,核心是:
用 CAS 操作将对象头恢复为原来的 Mark Word(从锁记录中取回),表示锁已释放。
🔁 解锁的 3 个步骤:
text深色版本1. 检查对象头是否还指向当前线程的锁记录
↓
2. 使用 CAS 将对象头的 Mark Word 恢复为锁记录中的 displaced Mark Word
↓
3. 如果 CAS 成功,解锁完成;失败则说明有竞争,需交给重量级锁处理
🧩 详细拆解:
步骤1:检查对象头是否指向当前线程的锁记录
- JVM 会检查对象头的 Mark Word 是否仍然指向当前线程栈中的 Lock Record。
- 如果是,说明这个锁还是“轻量级锁”状态,且由当前线程持有,可以尝试释放。
步骤2:CAS 恢复原始 Mark Word
- JVM 使用 CAS 操作,把对象头的 Mark Word 恢复为锁记录中保存的原始值(即
displaced Mark Word
)。 - 这个操作是原子的,确保不会被其他线程干扰。
步骤3:CAS 成功 or 失败?
- ✅ 成功:对象头恢复为无锁状态(或偏向锁状态),解锁完成。其他线程可以来竞争。
- ❌ 失败:说明在这期间,有其他线程尝试获取锁,并且已经升级为重量级锁(Monitor)。
🖼️ 解锁前后对比图
加锁后(线程持有锁):
text深色版本堆对象
┌─────────────┐
│ Object │
│ ┌─────────┐ │
│ │ Mark Word ├─→ 指向栈中的 Lock Record
│ └─────────┘ │
└─────────────┘
线程栈
┌─────────────────┐
│ 栈帧(Frame) │
│ │
│ ┌───────────┐ │
│ │ Lock Record │←┼── 被对象头指向
│ │ - displaced│
│ │ Mark Word│ ← 原始 Mark Word 的副本
│ └───────────┘ │
└─────────────────┘
解锁后(锁释放):
text深色版本堆对象
┌─────────────┐
│ Object │
│ ┌─────────┐ │
│ │ Mark Word │←─ 存放原来的值(无锁或可偏向状态)
│ └─────────┘ │
└─────────────┘
线程栈
┌─────────────────┐
│ 栈帧(Frame) │
│ │
│ ┌───────────┐ │
│ │ Lock Record │ ← 不再被引用,后续栈帧销毁时自动回收
│ └───────────┘ │
└─────────────────┘
🔁 锁记录本身不需要手动释放,它是在线程栈中的局部变量,随着方法执行结束,栈帧被弹出,自动销毁。
✅ 二、为什么用 CAS 恢复?
- 保证原子性:避免多个线程同时释放或竞争时出错。
- 检测是否被升级:如果 CAS 失败,说明对象头已经被修改(比如被升级为重量级锁),就不能再按轻量级锁处理。
✅ 三、举个例子(代码层面)
java深色版本public class Counter {
private int count = 0;
public void increment() {
synchronized (this) { // ① 加锁(可能进入轻量级锁)
count++;
} // ② 解锁!JVM 自动在这里释放锁
}
}
- 在
}
大括号结束时,JVM 会自动插入 解锁逻辑。 - 如果此时还是轻量级锁状态,就执行上面说的“CAS 恢复 Mark Word”;
- 如果已经升级为重量级锁,就调用
Monitor.exit()
。
✅ 四、总结:锁的生命周期
text深色版本 ┌─────────────┐
│ 无锁状态 │
└─────────────┘
↓
线程A获取锁(CAS)
↓
┌─────────────┐
│ 轻量级锁 │ ←─ 对象头指向 Lock Record
└─────────────┘
↓
线程A执行同步代码
↓
线程A退出同步块
↓
JVM 尝试 CAS 恢复 Mark Word
↓
成功?————是————→ 回到无锁状态
↓否
失败
↓
说明已升级为重量级锁
↓
交给 Monitor 处理释放
✅ 回答你的问题:
“当前线程执行完之后,会把这个锁取消吗?”
会的!而且是自动的。
- JVM 在
synchronized
块结束时,自动执行解锁逻辑; - 对于轻量级锁,就是 用 CAS 把对象头恢复为原来的 Mark Word;
- 如果发现锁已经被升级,就交给重量级锁机制处理;
- 整个过程对开发者透明,你不需要手动释放。
💡 一句话总结:
“加锁时备份 Mark Word,改头换面占锁位; 释放时 CAS 回原始值,自动恢复无人知; JVM 默默帮你管,线程安全不用愁。”
这就是 synchronized
高效又安全的秘密所在!