编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • JVM内存模型与对象
      • 类加载与双亲委派
      • 垃圾回收与GC调优
      • 异常体系与JVM机制
      • 字节码指令集javap实战
      • JIT编译与去优化机制
      • JVM性能诊断工具链
      • OOM八大现场全景剖析
      • JVM参数调优全景图
      • GraalVM与AOT编译原理
      • HashMap底层哈希设计
      • String不可变与常量池
      • ArrayList与LinkedList源码
      • ConcurrentHashMap并发
      • TreeMap与红黑树原理
      • LinkedHashMap与LRU实现
      • Java数字类型原理
      • Object通用方法的契约
      • 泛型擦除与类型系统
      • 枚举原理与最佳实践
      • 注解原理与编译期处理
      • Lambda与引用底层原理
      • Stream原理与流水线设计
      • Optional设计原理
      • Record密封类与模式
      • 反射机制与动态代理
      • MethodHandle与VarHandle
      • 三大字节码框架对比
      • JavaAgent与Instrumentation机制
      • AOP三种实现路线对比
      • synchronized与锁升级
        • 8.1 开篇疑问
        • 8.2 synchronized的使用方式
          • 8.2.1 三种加锁方式
          • 8.2.2 底层字节码分析
          • 8.2.3 Monitor监视器的本质
        • 8.3 对象头与锁状态
          • 8.3.1 Mark Word结构详解
          • 8.3.2 四种锁状态概览
          • 8.3.3 使用JOL工具观察对象头
        • 8.4 锁升级的完整过程
          • 8.4.1 偏向锁原理
          • 8.4.2 偏向锁的撤销过程
          • 8.4.3 偏向锁的批量重偏向与批量撤销
          • 8.4.4 轻量级锁原理
          • 8.4.5 轻量级锁的膨胀过程
          • 8.4.6 重量级锁原理
          • 8.4.7 锁升级全景图
        • 8.5 锁优化技术
          • 8.5.1 锁消除
          • 8.5.2 锁粗化
          • 8.5.3 自适应自旋
          • 8.5.4 逃逸分析与锁优化的关系
        • 8.6 synchronized的可重入性原理
        • 8.7 wait/notify机制与Monitor的关系
          • 8.7.1 wait和notify的底层原理
          • 8.7.2 为什么wait必须在synchronized块中调用
          • 8.7.3 虚假唤醒与正确使用姿势
        • 8.8 synchronized vs ReentrantLock深度对比
          • 8.8.1 功能对比
          • 8.8.2 AQS框架原理
          • 8.8.3 选择建议与使用场景
        • 8.9 synchronized的版本演进
        • 8.10 生产环境中的synchronized最佳实践
        • 8.11 总结与核心要点
      • volatile与JMM内存模型
      • 线程池核心源码设计
      • Thread线程生命周期
      • AQS同步框架源码
      • 并发锁三剑客
      • CAS和Atomic深入分析
      • 五大同步器对比
      • CompletableFuture异步
      • IO模型演进BIO到AIO
      • ByteBuffer与堆外内存
      • 序列化原理与替代方案
      • 文件IO与NIO.2
      • 面向对象的真意
      • JDK设计模式上
      • JDK设计模式下
      • SPI与模块化设计
  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 专栏博客
杨充
2026-06-02
目录

synchronized与锁升级

# 31.synchronized与锁升级

# 目录介绍

  • 8.1 开篇疑问
  • 8.2 synchronized的使用方式
    • 8.2.1 三种加锁方式
    • 8.2.2 底层字节码分析
    • 8.2.3 Monitor监视器的本质
  • 8.3 对象头与锁状态
    • 8.3.1 Mark Word结构详解
    • 8.3.2 四种锁状态概览
    • 8.3.3 使用JOL工具观察对象头
  • 8.4 锁升级的完整过程
    • 8.4.1 偏向锁原理
    • 8.4.2 偏向锁的撤销过程
    • 8.4.3 偏向锁的批量重偏向与批量撤销
    • 8.4.4 轻量级锁原理
    • 8.4.5 轻量级锁的膨胀过程
    • 8.4.6 重量级锁原理
    • 8.4.7 锁升级全景图
  • 8.5 锁优化技术
    • 8.5.1 锁消除
    • 8.5.2 锁粗化
    • 8.5.3 自适应自旋
    • 8.5.4 逃逸分析与锁优化的关系
  • 8.6 synchronized的可重入性原理
  • 8.7 wait/notify机制与Monitor的关系
    • 8.7.1 wait和notify的底层原理
    • 8.7.2 为什么wait必须在synchronized块中调用
    • 8.7.3 虚假唤醒与正确使用姿势
  • 8.8 synchronized vs ReentrantLock深度对比
    • 8.8.1 功能对比
    • 8.8.2 AQS框架原理
    • 8.8.3 选择建议与使用场景
  • 8.9 synchronized的版本演进
  • 8.10 生产环境中的synchronized最佳实践
  • 8.11 总结与核心要点

# 8.1 开篇疑问

疑惑:synchronized 是 Java 最常用的同步关键字,但它真的是"重量级锁"吗?经常听说"偏向锁、轻量级锁、重量级锁",它们到底是什么?锁是怎么存在对象头里的?synchronized 和 ReentrantLock 该怎么选?JDK 15 为什么要废弃偏向锁?

答疑:JDK 6 对 synchronized 做了革命性优化,引入了锁升级机制。在无竞争或轻度竞争下,synchronized 的性能与无锁几乎一样。理解锁升级,就能理解为什么现代 Java 中 synchronized 不再是性能杀手。

# 8.2 synchronized的使用方式

# 8.2.1 三种加锁方式

public class SyncDemo {
    private final Object lock = new Object();
    
    // 1. 修饰实例方法——锁是 this 对象
    public synchronized void instanceMethod() {
        // 等价于 synchronized(this) { ... }
    }
    
    // 2. 修饰静态方法——锁是 Class 对象
    public static synchronized void staticMethod() {
        // 等价于 synchronized(SyncDemo.class) { ... }
    }
    
    // 3. 修饰代码块——锁是指定对象
    public void blockMethod() {
        synchronized (lock) {
            // 临界区,只有持有 lock 的线程才能进入
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

最佳实践:优先使用代码块方式,锁对象使用 private final 修饰,避免外部访问导致的意外同步:

// 不推荐:锁 this,外部代码也可能 synchronized(obj) 导致竞争
public synchronized void method() { }

// 推荐:私有锁对象,外部无法干扰
private final Object lock = new Object();
public void method() {
    synchronized (lock) { }
}
1
2
3
4
5
6
7
8

# 8.2.2 底层字节码分析

同步代码块通过 monitorenter 和 monitorexit 字节码指令实现:

public void syncBlock() {
    synchronized (lock) {
        doSomething();
    }
}
1
2
3
4
5

编译后的字节码:

 0: aload_0
 1: getfield      #3    // Field lock:Ljava/lang/Object;
 4: dup
 5: astore_1
 6: monitorenter              // 获取 lock 的 monitor
 7: aload_0
 8: invokevirtual #4          // doSomething()
11: aload_1
12: monitorexit               // 正常退出时释放 monitor
13: goto          21
16: astore_2
17: aload_1
18: monitorexit               // 异常退出时也释放 monitor(保证不会死锁)
19: aload_2
20: athrow
21: return

Exception table:
   from    to  target type
       7    13    16   any
      16    19    16   any
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

关键点:

  • 编译器会生成两条 monitorexit:一条用于正常退出,一条用于异常退出
  • 异常表确保即使发生异常,monitor 也一定会被释放
  • 同步方法则在方法的 access_flags 中设置 ACC_SYNCHRONIZED 标志,JVM 在方法调用时自动获取/释放 monitor

# 8.2.3 Monitor监视器的本质

每个 Java 对象都关联一个 Monitor(监视器/管程),这是操作系统的概念。Monitor 是实现互斥与协作的基本构件。

ObjectMonitor 结构(HotSpot C++ 实现):
┌─────────────────────────────────────┐
│ _header      : Mark Word 的备份      │
│ _object      : 关联的 Java 对象      │
│ _owner       : 当前持有锁的线程       │
│ _recursions  : 重入计数              │
│ _count       : monitor 的进入次数    │
│ _WaitSet     : 调用 wait() 的线程集   │  ← wait/notify 协作
│ _cxq         : 最近到达的竞争线程队列  │  ← 单向链表
│ _EntryList   : 等待获取锁的线程集     │  ← 双向链表
│ _SpinFreq    : 自旋频率              │
│ _SpinClock   : 自旋周期              │
└─────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

Monitor 的工作流程:

线程请求锁
  → _owner == null?
    → 是:设为 _owner,获取成功
    → 否:进入 _cxq 或 _EntryList 等待
      → 被唤醒后重新竞争 _owner
  
线程调用 wait()
  → 释放 _owner,进入 _WaitSet 等待
  → 被 notify() 唤醒后,移到 _EntryList 重新竞争

线程释放锁
  → 清除 _owner
  → 唤醒 _EntryList 或 _cxq 中的线程
1
2
3
4
5
6
7
8
9
10
11
12
13

# 8.3 对象头与锁状态

# 8.3.1 Mark Word结构详解

锁信息存储在对象头的 Mark Word 中。64 位 JVM 的 Mark Word 布局:

┌──────────────────────────────────────────────────────────────────────┐
│                        Mark Word (64 bits)                           │
├─────────────────────────────────────────────┬──────┬─────┬──────────┤
│               具体内容(取决于锁状态)           │ 分代  │偏向  │ 锁标志   │
│                                             │ 年龄  │标志  │  位     │
│                                             │(4bit)│(1bit)│(2bit)  │
├─────────────────────────────────────────────┼──────┼─────┼──────────┤
│ 无锁:   unused(25) | hashcode(31)           │ age  │  0  │   01    │
│ 偏向锁: thread(54) | epoch(2)               │ age  │  1  │   01    │
│ 轻量级锁: Lock Record 指针 (62)              │      │     │   00    │
│ 重量级锁: ObjectMonitor 指针 (62)            │      │     │   10    │
│ GC 标记:                                     │      │     │   11    │
└─────────────────────────────────────────────┴──────┴─────┴──────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

关键细节:

  • 无锁状态存储了 identity hashCode(调用 Object.hashCode() 后生成)
  • 一旦进入偏向锁,hashcode 字段被 threadId 替换。如果已经计算过 hashCode 的对象无法进入偏向锁
  • GC 分代年龄用 4 bit 表示,最大值 15,这就是 MaxTenuringThreshold 默认为 15 的原因

# 8.3.2 四种锁状态概览

锁状态 标志位 适用场景 性能 Mark Word 存储
无锁 01(偏向=0) 没有同步需求 最快 hashCode + 分代年龄
偏向锁 01(偏向=1) 只有一个线程访问 接近无锁 线程ID + epoch
轻量级锁 00 两个线程交替访问 CAS 操作 Lock Record 指针
重量级锁 10 多线程竞争激烈 内核态切换 Monitor 指针

核心原则:锁只能升级不能降级。无锁 → 偏向锁 → 轻量级锁 → 重量级锁。

# 8.3.3 使用JOL工具观察对象头

Java Object Layout(JOL)工具可以直接查看对象的内存布局:

<!-- Maven 依赖 -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.16</version>
</dependency>
1
2
3
4
5
6
import org.openjdk.jol.info.ClassLayout;

public class MarkWordDemo {
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        
        // 查看无锁状态
        System.out.println("无锁状态:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        
        // 加 synchronized 后查看
        synchronized (obj) {
            System.out.println("加锁状态:");
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }
        
        // 计算 hashCode 后查看
        System.out.println("hashCode: " + Integer.toHexString(obj.hashCode()));
        System.out.println("计算hashCode后:");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

输出示例:

无锁状态:
OFFSET  SIZE   TYPE DESCRIPTION                VALUE
     0     4        (object header)            05 00 00 00  (偏向锁可偏向状态)
     4     4        (object header)            00 00 00 00
     8     4        (object header)            ...

加锁状态:
     0     4        (object header)            05 f0 01 03  (偏向锁,存储线程ID)
1
2
3
4
5
6
7
8

# 8.4 锁升级的完整过程

# 8.4.1 偏向锁原理

设计动机:HotSpot 团队研究发现,大部分锁在整个生命周期中只被一个线程持有。偏向锁让这个线程以后每次进入同步块时无需做任何同步操作。

获取过程:

线程A 首次进入 synchronized 块
  │
  ↓ 检查 Mark Word 是否为可偏向状态(偏向标志=1,线程ID=0)
  │
  ↓ CAS 将线程A 的 ID 写入 Mark Word
  │
  ↓ 成功 → 获得偏向锁
  │
线程A 后续进入同一个 synchronized 块
  │
  ↓ 只需检查 Mark Word 中的线程 ID == 线程A 的 ID?
  │
  ↓ 是 → 直接进入,无需任何 CAS 或同步操作
  │       这就是"偏向"的含义——偏向于第一个获取它的线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14

性能数据:偏向锁在无竞争场景下,进入同步块的开销约为 1~2 纳秒,几乎与无锁相同。

# 8.4.2 偏向锁的撤销过程

当另一个线程尝试获取偏向锁时,触发撤销:

线程B 尝试获取偏向锁
  │
  ↓ 发现 Mark Word 中的线程 ID ≠ 线程B
  │
  ↓ 需要撤销偏向锁(必须等到安全点 Safe Point)
  │
  ↓ 暂停持有偏向锁的线程A(STW)
  │
  ↓ 检查线程A 的状态:
    │
    ├─ 线程A 已退出同步块 → 撤销偏向,Mark Word 恢复为无锁
    │   → 线程B 通过 CAS 竞争轻量级锁
    │
    └─ 线程A 仍在同步块中 → 升级为轻量级锁
        → 在线程A 的栈帧中创建 Lock Record
        → Mark Word 指向 Lock Record
        → 线程B 也创建 Lock Record 并竞争
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

关键问题:偏向锁的撤销需要 STW(暂停所有线程到安全点),这在高并发场景下开销不可忽视。

# 8.4.3 偏向锁的批量重偏向与批量撤销

JVM 对偏向锁的撤销做了优化:

批量重偏向(Bulk Rebias):

  • 当同一个类的对象的偏向锁被频繁撤销(默认阈值 20 次),JVM 认为该类的锁"确实会被多个线程使用"
  • 此后新创建的该类对象的偏向锁会偏向最近的线程(通过 epoch 机制实现)

批量撤销(Bulk Revoke):

  • 当同一个类的撤销次数达到更高阈值(默认 40 次),JVM 彻底放弃该类的偏向锁
  • 该类所有新旧对象都不再使用偏向锁
# 相关 JVM 参数
-XX:BiasedLockingBulkRebiasThreshold=20    # 批量重偏向阈值
-XX:BiasedLockingBulkRevokeThreshold=40    # 批量撤销阈值
-XX:BiasedLockingDecayTime=25000           # 衰退时间(ms)
1
2
3
4

# 8.4.4 轻量级锁原理

设计动机:多个线程在不同时段交替获取锁(没有真正的竞争),用 CAS 代替重量级的互斥量。

获取过程详解:

线程A 尝试获取轻量级锁
  │
  ↓ 步骤1:在线程A 的栈帧中创建 Lock Record
  │         Lock Record 包含:
  │         - Displaced Mark Word(Mark Word 的备份)
  │         - owner(指向锁对象的指针)
  │
  ↓ 步骤2:CAS 尝试将对象 Mark Word 替换为指向 Lock Record 的指针
  │
  ↓ 步骤3:
    │
    ├─ CAS 成功 → 获得轻量级锁,Mark Word 锁标志位变为 00
    │
    └─ CAS 失败 → 检查 Mark Word 是否指向当前线程的栈帧
        │
        ├─ 是 → 重入(在栈帧中再创建一个 Lock Record,Displaced Mark Word = null)
        │
        └─ 否 → 有竞争!开始自旋
            │
            ├─ 自旋成功(对方释放了锁)→ 获得轻量级锁
            │
            └─ 自旋失败(超过阈值)→ 膨胀为重量级锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
线程栈帧:                    对象头:
┌──────────────┐          ┌──────────────┐
│ Lock Record  │←─────────│ Mark Word    │
│ ┌──────────┐ │  CAS替换  │ (指向锁记录) │
│ │Displaced │ │          │ 锁标志: 00   │
│ │Mark Word │ │          └──────────────┘
│ └──────────┘ │
│ owner ───────┼──────────→ 锁对象
└──────────────┘

重入时:栈中有多个 Lock Record 指向同一个对象
┌──────────────┐
│ Lock Record 3│  Displaced = null(重入标记)
│ Lock Record 2│  Displaced = null
│ Lock Record 1│  Displaced = 原始 Mark Word
└──────────────┘
释放时从上往下弹出,最后一个恢复 Mark Word
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 8.4.5 轻量级锁的膨胀过程

轻量级锁膨胀为重量级锁
  │
  ↓ 步骤1:分配 ObjectMonitor 对象(C++ 层面,从全局 Monitor 缓存池中获取)
  │
  ↓ 步骤2:将 ObjectMonitor 的 _header 设为原始 Mark Word
  │         将 ObjectMonitor 的 _object 指向锁对象
  │
  ↓ 步骤3:CAS 将对象 Mark Word 替换为指向 ObjectMonitor 的指针
  │         锁标志位变为 10
  │
  ↓ 步骤4:当前竞争失败的线程进入 ObjectMonitor 的 _EntryList
  │         操作系统层面阻塞(park/unpark)
1
2
3
4
5
6
7
8
9
10
11
12

# 8.4.6 重量级锁原理

重量级锁依赖操作系统的 Mutex Lock(互斥量),涉及用户态到内核态的切换(上下文切换约 10~20 微秒):

ObjectMonitor 的工作流程:
┌─────────────────────────────────────────┐
│                                         │
│  请求锁 → _owner == null?               │
│            YES → 设为 _owner             │
│            NO  → 进入 _cxq 队列         │
│                  ↓                       │
│            自旋尝试(_SpinFreq 次)       │
│                  ↓ 自旋失败              │
│            park() 阻塞当前线程           │
│                                         │
│  释放锁 → _owner = null                 │
│          → 唤醒 _EntryList 或 _cxq      │
│            中的一个线程 unpark()          │
│                                         │
│  wait() → 释放 _owner                   │
│          → 进入 _WaitSet                 │
│          → park()                        │
│                                         │
│  notify() → 从 _WaitSet 取出一个        │
│            → 放入 _EntryList             │
│                                         │
└─────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

性能开销分析:

  • 线程阻塞(park):用户态 → 内核态切换,约 10 微秒
  • 线程唤醒(unpark):内核态 → 用户态切换,约 10 微秒
  • 上下文切换:保存/恢复 CPU 寄存器,约 5~10 微秒
  • 总计一次锁竞争:约 20~40 微秒(相比偏向锁的 1~2 纳秒差了万倍)

# 8.4.7 锁升级全景图

新创建的对象
  │
  ↓ JVM 启动后 4 秒(BiasedLockingStartupDelay)
  │ 4秒内创建的对象 → 直接无锁(不可偏向)
  │ 4秒后创建的对象 → 匿名偏向(偏向标志=1,线程ID=0)
  │
  ↓ 线程A 首次加锁
偏向锁(Mark Word 存储线程A 的 ID)
  │
  ↓ 线程B 尝试获取
撤销偏向锁(到达安全点 STW)
  │
  ├─ 线程A 已退出同步块 → 撤销偏向,CAS 竞争
  └─ 线程A 仍在同步块 → 升级
  │
  ↓ CAS 替换 Mark Word
轻量级锁(Mark Word 指向栈中 Lock Record)
  │
  ↓ CAS 自旋失败(竞争激烈)
膨胀为重量级锁(Mark Word 指向 ObjectMonitor)
  │
  ↓ 所有线程释放锁
  │ Mark Word 恢复
无锁状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

特殊路径:

  • 如果对象已调用 hashCode(),无法进入偏向锁(偏向锁的 Mark Word 没有空间存 hashCode)
  • 调用 wait()/notify() 会直接膨胀为重量级锁(这些方法依赖 ObjectMonitor 的 _WaitSet)

# 8.5 锁优化技术

# 8.5.1 锁消除

JIT 编译器通过逃逸分析发现锁对象不会被其他线程访问,直接消除锁操作:

// 场景1:局部变量上的锁
public String concat(String s1, String s2) {
    StringBuffer sb = new StringBuffer();  // sb 不会逃逸出方法
    sb.append(s1);   // StringBuffer.append 是 synchronized 的
    sb.append(s2);
    return sb.toString();
}
// JIT 优化后:消除 StringBuffer 内部的所有 synchronized

// 场景2:循环中的局部锁
public void process() {
    Object lock = new Object();  // 每次调用创建新的 lock
    synchronized (lock) {        // 这个锁对象不可能被其他线程访问
        doWork();
    }
}
// JIT 优化后:完全消除 synchronized
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 相关 JVM 参数
-XX:+DoEscapeAnalysis        # 开启逃逸分析(默认开启)
-XX:+EliminateLocks          # 开启锁消除(默认开启)
1
2
3

# 8.5.2 锁粗化

将连续的加锁解锁合并为一次更大范围的锁:

// 优化前:连续多次加锁解锁
public void addAll(List<String> items) {
    for (String item : items) {
        synchronized (lock) {
            list.add(item);
        }
    }
}
// 每次循环都要 monitorenter/monitorexit

// JIT 优化后:粗化为一次加锁
public void addAll(List<String> items) {
    synchronized (lock) {
        for (String item : items) {
            list.add(item);
        }
    }
}
// 只需一次 monitorenter/monitorexit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

注意:锁粗化也有副作用——锁持有时间变长,可能增加其他线程的等待时间。JIT 会权衡利弊。

# 8.5.3 自适应自旋

轻量级锁 CAS 失败后,线程不会立即阻塞(阻塞/唤醒开销大),而是自旋(忙等待,反复 CAS 尝试):

// 自旋的本质(伪代码)
while (!cas(lockAddr, expected, myLockRecord)) {
    // CAS 失败,继续尝试
    spinCount++;
    if (spinCount > adaptiveSpinLimit) {
        // 自旋超过阈值,放弃自旋,膨胀为重量级锁
        inflate();
        break;
    }
}
1
2
3
4
5
6
7
8
9
10

自适应的含义:

  • 如果在同一个锁上,上次自旋成功获取了锁 → 认为自旋很可能成功 → 增加自旋次数
  • 如果上次自旋失败了 → 认为自旋大概率浪费 → 减少自旋次数甚至跳过自旋

JVM 通过运行时统计数据动态调整,无需手动配置。

# 8.5.4 逃逸分析与锁优化的关系

逃逸分析是 JIT 编译器的一项关键优化技术,它分析对象的动态作用域:

逃逸分析结果    →    可能的优化
─────────────────────────────────
不逃逸(方法内局部)→ 锁消除、栈上分配、标量替换
线程逃逸(多线程访问)→ 不做优化
1
2
3
4
// 不逃逸
public void demo() {
    Object lock = new Object();    // lock 不逃逸
    synchronized (lock) { }         // 锁消除
}

// 线程逃逸
private Object sharedLock = new Object();
public void demo() {
    synchronized (sharedLock) { }   // 可能被多线程访问,不能消除
}
1
2
3
4
5
6
7
8
9
10
11

# 8.6 synchronized的可重入性原理

synchronized 是可重入锁——同一个线程可以多次获取同一把锁:

public class ReentrantDemo {
    public synchronized void methodA() {
        methodB();  // 在持有 this 锁的情况下调用另一个 synchronized 方法
    }
    
    public synchronized void methodB() {
        // 如果不可重入,这里会死锁!
        // 线程在 methodA 中持有 this 锁,进入 methodB 又需要 this 锁
    }
}
1
2
3
4
5
6
7
8
9
10

实现原理:

偏向锁的可重入:Mark Word 中的线程 ID 等于当前线程,直接进入。

轻量级锁的可重入:每次重入在栈帧中压入一个新的 Lock Record(Displaced Mark Word 为 null 作为重入标记)。释放时从上往下弹出 Lock Record,最后一个(Displaced Mark Word 非 null)才真正解锁。

重量级锁的可重入:ObjectMonitor 的 _recursions 计数器。每次重入加 1,退出减 1,减到 0 时释放锁。

// 重入深度为 3 时的栈和 Monitor 状态
// 轻量级锁:
//   栈帧: LR3(null) | LR2(null) | LR1(原始MW)
//   释放顺序: LR3→LR2→LR1(最后恢复 Mark Word)

// 重量级锁:
//   Monitor._owner = 当前线程
//   Monitor._recursions = 3
//   释放时 _recursions-- 直到 0,才真正释放 _owner
1
2
3
4
5
6
7
8
9

# 8.7 wait/notify机制与Monitor的关系

# 8.7.1 wait和notify的底层原理

wait() / notify() / notifyAll() 是 Object 的方法,底层依赖 ObjectMonitor:

调用 obj.wait():
  1. 当前线程必须是 obj 的 Monitor 的 _owner
  2. 释放 Monitor(_owner = null, _recursions = 0)
  3. 当前线程进入 _WaitSet(双向链表)
  4. 线程被阻塞(park)

调用 obj.notify():
  1. 从 _WaitSet 中取出一个线程
  2. 将其移到 _EntryList 或 _cxq
  3. 被移出的线程等待重新竞争 Monitor
  4. 注意:notify() 不会立即释放锁!
     调用 notify() 的线程继续执行,直到退出 synchronized 块

调用 obj.notifyAll():
  1. 将 _WaitSet 中的所有线程移到 _EntryList
  2. 所有线程竞争 Monitor,只有一个能获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 8.7.2 为什么wait必须在synchronized块中调用

// 如果不在 synchronized 中调用 wait()
// 会抛出 IllegalMonitorStateException

// 原因:wait() 需要释放 Monitor,如果没有持有 Monitor 怎么释放?
// 更重要的是防止 lost wake-up(丢失唤醒)问题:

// 如果 wait 不需要同步:
// 线程A 检查条件为 false → 准备 wait()
// 但在执行 wait() 之前,线程B 修改条件为 true 并调用 notify()
// 线程A 执行 wait(),但 notify 已经过去了
// 线程A 永远等待 → 丢失唤醒!

// synchronized 保证了"检查条件"和"进入等待"是原子操作
synchronized (lock) {
    while (!condition) {   // 检查条件
        lock.wait();       // 在同一个锁保护下等待
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 8.7.3 虚假唤醒与正确使用姿势

// 错误:使用 if 检查条件
synchronized (lock) {
    if (!condition) {
        lock.wait();  // 虚假唤醒后直接往下执行,条件可能仍然不满足!
    }
    // 处理...
}

// 正确:使用 while 循环检查条件
synchronized (lock) {
    while (!condition) {    // 被唤醒后重新检查条件
        lock.wait();
    }
    // 条件确实满足了,安全处理
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

虚假唤醒的来源:

  1. 操作系统底层:pthread_cond_wait 在某些系统上可能无原因返回
  2. notifyAll:唤醒所有等待线程,但只有一个能满足条件
  3. 条件变化:被唤醒时,条件可能已被其他线程改变

# 8.8 synchronized vs ReentrantLock深度对比

# 8.8.1 功能对比

特性 synchronized ReentrantLock
实现层面 JVM 内置(字节码 + C++) Java API(基于 AQS)
释放方式 自动释放(退出同步块/异常) 必须手动 unlock()(try-finally)
可中断获取 不可中断 lockInterruptibly()
超时获取 不支持 tryLock(timeout, unit)
非阻塞获取 不支持 tryLock()
公平锁 非公平 可选公平/非公平
条件变量 只有一个条件(wait/notify) 多个 Condition
锁升级 偏向→轻量级→重量级 无升级概念(直接 CAS + park)
性能 JDK 6+ 优化后与 Lock 接近 稳定

# 8.8.2 AQS框架原理

ReentrantLock 基于 AQS(AbstractQueuedSynchronizer) 实现:

AQS 核心结构:
┌───────────────────────────────┐
│ state: int(锁状态)            │  0=未锁定, >0=锁定(重入次数)
│ head → Node → Node → tail    │  CLH 变体队列(双向链表)
│        (等待获取锁的线程队列)    │
└───────────────────────────────┘

获取锁过程(非公平模式):
1. CAS 尝试将 state 从 0 改为 1
   → 成功:获取锁,设为 exclusiveOwnerThread
   → 失败:进入队列
2. 入队后自旋 + park 等待
3. 前驱节点释放锁时 unpark 后继节点

释放锁过程:
1. state - 1(支持重入)
2. state == 0 时真正释放
3. unpark 头节点的后继节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 8.8.3 选择建议与使用场景

优先使用 synchronized:

  • 代码更简洁,不用担心忘记 unlock
  • JVM 可以做锁升级、锁消除等优化
  • 未来 JVM 版本可能进一步优化
  • 适合大多数简单同步场景

选择 ReentrantLock 的场景:

// 1. 需要超时获取锁
if (lock.tryLock(5, TimeUnit.SECONDS)) {
    try { doWork(); }
    finally { lock.unlock(); }
} else {
    handleTimeout();
}

// 2. 需要可中断获取锁
try {
    lock.lockInterruptibly();
    try { doWork(); }
    finally { lock.unlock(); }
} catch (InterruptedException e) {
    handleInterruption();
}

// 3. 需要多个条件变量(生产者-消费者)
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

// 生产者等待 notFull,唤醒 notEmpty
// 消费者等待 notEmpty,唤醒 notFull
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 8.9 synchronized的版本演进

版本 变化 影响
JDK 1.0 synchronized 诞生 纯重量级锁实现
JDK 1.4 引入自旋锁 减少线程阻塞开销
JDK 1.6 锁升级(偏向锁+轻量级锁) 性能大幅提升
JDK 1.6 锁消除、锁粗化 编译器级别优化
JDK 1.6 自适应自旋 智能调整自旋次数
JDK 15 默认关闭偏向锁 -XX:-UseBiasedLocking
JDK 18 废弃偏向锁 偏向锁代码维护成本高,现代应用中收益递减

JDK 15 废弃偏向锁的原因:

  1. 偏向锁的撤销需要 STW,在高并发应用中反而有害
  2. 偏向锁的代码复杂度高(占 HotSpot 同步代码的 ~10%),维护成本大
  3. 现代应用中单线程访问锁的场景减少(微服务、响应式编程)
  4. 轻量级锁的 CAS 开销在现代 CPU 上已经很小

JDK 21 虚拟线程对 synchronized 的影响:

// 虚拟线程中使用 synchronized 会导致"线程钉扎"(Pinning)
// 即虚拟线程被钉在载体线程上,无法切换

// ❌ 虚拟线程中使用 synchronized 会阻塞载体线程
Thread.startVirtualThread(() -> {
    synchronized (lock) {
        // 虚拟线程被钉在载体线程上
        // 如果这里有 IO 操作,载体线程也被阻塞
        blockingIO();
    }
});

// ✅ 虚拟线程中推荐使用 ReentrantLock
Thread.startVirtualThread(() -> {
    lock.lock();
    try {
        blockingIO();  // 虚拟线程可以正常切换
    } finally {
        lock.unlock();
    }
});

// JDK 24 正在改进 synchronized 对虚拟线程的支持
// 未来可能不再有钉扎问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

核心启示:synchronized 在 JVM 中持续演进。理解其底层原理,才能在不同版本、不同场景下做出最优选择。

# 8.10 生产环境中的synchronized最佳实践

1. 锁对象的选择:

// 不推荐:锁 this
public synchronized void method() { }
// 外部可以 synchronized(obj) 导致意外竞争

// 不推荐:锁 Class
public static synchronized void method() { }
// 所有实例共享同一把锁

// 推荐:专用锁对象
private final Object lock = new Object();
// 或更好的选择(JDK 14+)
// private final Object lock = new Object() {};  // 匿名内部类,有独特的类名,便于排查
1
2
3
4
5
6
7
8
9
10
11
12

2. 减小锁粒度:

// 不推荐:方法级别的锁
public synchronized void process(Request req) {
    validate(req);     // 不需要同步
    compute(req);      // 不需要同步
    updateCache(req);  // 需要同步
}

// 推荐:只锁必要的部分
public void process(Request req) {
    validate(req);
    compute(req);
    synchronized (cacheLock) {
        updateCache(req);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

3. 避免死锁:

// 死锁场景
// 线程A: synchronized(lockA) { synchronized(lockB) { } }
// 线程B: synchronized(lockB) { synchronized(lockA) { } }

// 解决方案1:固定加锁顺序
// 总是先锁 lockA 再锁 lockB

// 解决方案2:使用 tryLock 超时
// lock.tryLock(timeout) 避免永久等待

// 解决方案3:使用专门的并发工具
// ConcurrentHashMap、CopyOnWriteArrayList 等
1
2
3
4
5
6
7
8
9
10
11
12

# 8.11 总结与核心要点

锁升级的设计哲学:

  1. 乐观到悲观:偏向锁假设无竞争,轻量级锁假设轻度竞争,重量级锁应对激烈竞争
  2. 按需升级:从零开销的偏向锁开始,只在真正需要时才付出更大代价
  3. 空间换时间:Mark Word 复用不同状态的位,用极少空间支持四种锁状态
  4. 持续演进:从全程 STW 到偏向锁到废弃偏向锁,JVM 团队持续根据真实工作负载调优

核心要点速查:

锁状态 Mark Word 存储 加锁方式 开销 适用场景
偏向锁 线程 ID 比较线程 ID ~1ns 单线程反复进入
轻量级锁 Lock Record 指针 CAS 操作 ~10ns 线程交替执行
重量级锁 Monitor 指针 Mutex + 内核切换 ~10μs 多线程激烈竞争

面试核心回答框架:

Q: synchronized 的实现原理?
A: 1) 字节码层面:monitorenter/monitorexit
   2) 对象头层面:Mark Word 存储锁状态
   3) JVM 层面:锁升级机制(偏向→轻量级→重量级)
   4) 优化层面:锁消除、锁粗化、自适应自旋
   5) 底层实现:ObjectMonitor(C++),依赖 Mutex
1
2
3
4
5
6

理解锁升级,就能理解为什么 synchronized 在现代 JVM 中性能已经足够好,以及什么场景下应该选择 ReentrantLock。

上次更新: 2026/06/10, 11:13:41
AOP三种实现路线对比
volatile与JMM内存模型

← AOP三种实现路线对比 volatile与JMM内存模型→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式