偏向锁、轻量级锁、自旋锁、重量级锁 是什么意思,请给我详细介绍


偏向锁轻量级锁自旋锁 是 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 操作 + 栈帧锁记录 实现,避免进入操作系统级别的重量级锁。

🧠 原理:

  1. 线程在自己的栈帧中创建一个 锁记录(Lock Record)
  2. JVM 使用 CAS 将对象头的 Mark Word 复制到锁记录中;
  3. 再用 CAS 将对象头指向锁记录(表示加锁成功);
  4. 如果 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,判断当前锁状态(无锁、偏向、轻量级、重量级)。



六、使用建议与最佳实践

✅ 推荐做法:

  1. 优先使用 synchronized:JVM 优化充分,代码简洁;
  2. 不要手动干预锁升级:这是 JVM 的职责;
  3. 减少锁的粒度:只锁必要的代码段;
  4. 避免在锁中做耗时操作(如 IO、网络请求);
  5. 高并发场景可考虑 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 失败的原因:

  • 另一个线程已经成功修改了对象头(即已经加锁);
  • 此时存在多线程竞争

🔄 接下来会发生什么?

  1. 当前线程发现 CAS 失败,说明轻量级锁拿不到;
  2. JVM 会尝试 自旋(Spinning):循环几次,看看持有锁的线程是否很快释放;
  3. 如果自旋一定次数后仍失败(默认 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 高效又安全的秘密所在!