编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • 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与锁升级
      • volatile与JMM内存模型
      • 线程池核心源码设计
      • Thread线程生命周期
      • AQS同步框架源码
      • 并发锁三剑客
        • 1. 案例引入
          • 1.1 缓存场景的"读写之痛"
          • 1.2 RWLock 升级死锁的真实事故
          • 1.3 我们要回答什么
        • 2. ReentrantLock 工程化深挖
          • 2.1 与 synchronized 的六维对比
          • 2.2 公平/非公平选型决策树
          • 2.3 lockInterruptibly 的中断契约
          • 2.4 tryLock 超时的正确用法
          • 2.5 Condition 多条件队列高级用法
        • 3. ReentrantReadWriteLock 源码剖析
          • 3.1 高低位编码的状态布局
          • 3.2 读锁获取链路
          • 3.3 写锁获取与重入
          • 3.4 锁降级(写 → 读)的标准范式
          • 3.5 锁升级(读 → 写)为什么死锁
          • 3.6 RWLock 的"写饥饿"难题
        • 4. StampedLock 乐观读革命
          • 4.1 三种锁模式
          • 4.2 stamp 戳的设计
          • 4.3 乐观读的标准范式
          • 4.4 validate 与内存屏障
          • 4.5 锁转换 API(tryConvertToWriteLock)
          • 4.6 StampedLock 的四大坑
        • 5. 三剑客性能横评
          • 5.1 测试场景设计
          • 5.2 读多写少(95:5)
          • 5.3 读写均衡(50:50)
          • 5.4 写多读少(5:95)
          • 5.5 横评结论与选型决策树
        • 6. Loom 时代的 Lock 选型
          • 6.1 synchronized 的 Pinning 痛点
          • 6.2 Lock 三剑客的虚拟线程友好性
          • 6.3 JDK 24 的 synchronized 解 Pin
        • 7. 工程实战避坑
          • 7.1 lock 必须配 try-finally
          • 7.2 锁顺序与死锁四要素
          • 7.3 锁粒度的"先粗后细"原则
          • 7.4 监控埋点:getQueueLength 与 isLocked
        • 8. 综合回扣与设计哲学
          • 8.1 案例真相揭晓
          • 8.2 三剑客设计哲学
          • 8.3 速查表与下一篇预告
      • CAS和Atomic深入分析
      • 五大同步器对比
      • CompletableFuture异步
      • IO模型演进BIO到AIO
      • ByteBuffer与堆外内存
      • 序列化原理与替代方案
      • 文件IO与NIO.2
      • 面向对象的真意
      • JDK设计模式上
      • JDK设计模式下
      • SPI与模块化设计
  • Go入门到精通

  • JavaScript入门

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

并发锁三剑客

# 36.并发锁三剑客

# 目录介绍

  • 1. 案例引入
    • 1.1 缓存场景的"读写之痛"
    • 1.2 RWLock 升级死锁的真实事故
    • 1.3 我们要回答什么
  • 2. ReentrantLock 工程化深挖
    • 2.1 与 synchronized 的六维对比
    • 2.2 公平/非公平选型决策树
    • 2.3 lockInterruptibly 的中断契约
    • 2.4 tryLock 超时的正确用法
    • 2.5 Condition 多条件队列高级用法
  • 3. ReentrantReadWriteLock 源码剖析
    • 3.1 高低位编码的状态布局
    • 3.2 读锁获取链路
    • 3.3 写锁获取与重入
    • 3.4 锁降级(写 → 读)的标准范式
    • 3.5 锁升级(读 → 写)为什么死锁
    • 3.6 RWLock 的"写饥饿"难题
  • 4. StampedLock 乐观读革命
    • 4.1 三种锁模式
    • 4.2 stamp 戳的设计
    • 4.3 乐观读的标准范式
    • 4.4 validate 与内存屏障
    • 4.5 锁转换 API(tryConvertToWriteLock)
    • 4.6 StampedLock 的四大坑
  • 5. 三剑客性能横评
    • 5.1 测试场景设计
    • 5.2 读多写少(95:5)
    • 5.3 读写均衡(50:50)
    • 5.4 写多读少(5:95)
    • 5.5 横评结论与选型决策树
  • 6. Loom 时代的 Lock 选型
    • 6.1 synchronized 的 Pinning 痛点
    • 6.2 Lock 三剑客的虚拟线程友好性
    • 6.3 JDK 24 的 synchronized 解 Pin
  • 7. 工程实战避坑
    • 7.1 lock 必须配 try-finally
    • 7.2 锁顺序与死锁四要素
    • 7.3 锁粒度的"先粗后细"原则
    • 7.4 监控埋点:getQueueLength 与 isLocked
  • 8. 综合回扣与设计哲学
    • 8.1 案例真相揭晓
    • 8.2 三剑客设计哲学
    • 8.3 速查表与下一篇预告

# 1. 案例引入

# 1.1 缓存场景的"读写之痛"

某电商配置中心:1 个写线程秒级更新规则,800 个读线程毫秒级查询。一开始用 synchronized:

public class ConfigCache {
    private Map<String, Rule> rules = new HashMap<>();

    public synchronized Rule get(String key) { return rules.get(key); }    // ← 800 个读线程互斥
    public synchronized void put(String key, Rule v) { rules.put(key, v); }
}
1
2
3
4
5
6

压测一上:P99 延迟 480ms,CPU 仅 30%——大量线程在 BLOCKED 态空等。换成 ReentrantReadWriteLock:

private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public Rule get(String key) {
    rwLock.readLock().lock();
    try { return rules.get(key); }
    finally { rwLock.readLock().unlock(); }
}
1
2
3
4
5
6

P99 降到 80ms,看似搞定——但继续加大写线程到 4 个,读线程的 P99 又飙到 220ms。再换 StampedLock 乐观读,P99 降到 12ms,吞吐再翻 5 倍。

为什么三个 Lock 性能差距这么大?什么场景该用谁? —— 这是 37 篇要回答的核心。

# 1.2 RWLock 升级死锁的真实事故

某线上事故复盘:缓存类用了 ReentrantReadWriteLock,业务希望"读到旧数据时立即升级为写锁刷新":

rwLock.readLock().lock();
try {
    Rule r = cache.get(key);
    if (r.isExpired()) {
        rwLock.writeLock().lock();      // ❌ 读 → 写升级
        try {
            cache.put(key, fetch(key));
        } finally { rwLock.writeLock().unlock(); }
    }
} finally { rwLock.readLock().unlock(); }
1
2
3
4
5
6
7
8
9
10

生产环境跑着跑着所有线程卡死——jstack 一看全在 writeLock().lock()。原因 36 篇 §8.2 已揭过:读锁升级写锁会死锁——A 持读锁等写锁,要等所有读锁释放;但 B 也持读锁也在等写锁;两人无限死等。

怎么改? §3.4、§3.5、§4.5 给出三种方案。

# 1.3 我们要回答什么

37 篇是卷五第 6 篇,承接 36 篇 AQS 总框架,把 Lock API 全家桶的工程化用法一次讲透:

36.AQS (地基)  →  37.三剑客 (本篇/Lock 全家桶)  →  38.CAS  →  39.线程池
1

带着 6 个问题展开:

追问 ①:ReentrantLock 比 synchronized 强在哪?什么时候必须用?     → §2
追问 ②:RWLock 高低位编码的实现细节?读多场景为什么还不够快?     → §3
追问 ③:StampedLock 乐观读到底"乐观"在哪里?性能凭什么暴涨?      → §4
追问 ④:三剑客在不同读写比下性能曲线长什么样?                     → §5
追问 ⑤:Loom 时代 synchronized 真的废了吗?                       → §6
追问 ⑥:怎么写一行不死锁、不饿死、可监控的并发代码?               → §7
1
2
3
4
5
6

# 2. ReentrantLock 工程化深挖

# 2.1 与 synchronized 的六维对比

维度 synchronized ReentrantLock
实现层 JVM 字节码 + C++ monitor Java 层 + AQS(36 篇)
中断响应 ❌ 无法中断 ✅ lockInterruptibly()
超时获取 ❌ 无 ✅ tryLock(time, unit)
公平模式 ❌ 仅非公平 ✅ 构造时可选
条件队列 1 个(monitor) N 个(多 Condition)
释放方式 自动(出 synchronized 块) 手动(必须 finally unlock)

结论:synchronized 是"傻瓜锁"——简单但功能有限;ReentrantLock 是"瑞士军刀"——灵活但要自己拧螺丝。JDK 6 锁升级后,无竞争时 synchronized 性能持平甚至略优,但只要有以下需求之一,必须用 ReentrantLock:

✅ 需要响应中断(如长任务里支持取消)
✅ 需要超时获取(如限时尝试拿锁,失败走降级逻辑)
✅ 需要公平锁(如严格 FIFO 防饥饿)
✅ 需要多条件队列(如生产者消费者多条件区分)
✅ 需要尝试锁(tryLock() 不阻塞直接返回)
1
2
3
4
5

# 2.2 公平/非公平选型决策树

36 篇 §8.1 已拆解过差异——只在 initialTryLock 里多调一次 hasQueuedPredecessors()。对应到生产选型:

是否有线程长期被饿死的真实风险?
├── 是(如关键资源、SLA 敏感场景)
│    └── 公平锁 new ReentrantLock(true)
└── 否(默认 95% 业务)
     └── 非公平锁 new ReentrantLock()  ← 默认值,吞吐高 2-4 倍
1
2
3
4
5

为什么非公平默认就够:现实中线程切换、调度抖动天然带来"伪公平"——线程 A 释放锁后 B 立即抢到的概率不会太高,长期下来分布相对均匀。公平锁的代价:每次 acquire 都要查队列、入队,吞吐损失约 40%~70%。

# 2.3 lockInterruptibly 的中断契约

35 篇 §5 讲过中断三件套——这里看 AQS 怎么落地:

public final void acquireInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted()) throw new InterruptedException();         // ① 入口检查
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);                                   // ② 等待中也响应
}

private void doAcquireInterruptibly(int arg) throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; return; }
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                throw new InterruptedException();                       // ★ park 期间被中断直接抛
        }
    } catch (Throwable t) { cancelAcquire(node); throw t; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

与 lock() 的关键差异:lock() 即使 park 期间被中断也只是记录标志、继续等待,等拿到锁后由 selfInterrupt() 补打;lockInterruptibly() 立即抛 InterruptedException——这是"长任务可取消"的物理基础。

// 工程范式:长任务支持中断退出
lock.lockInterruptibly();              // ★ 必须用 Interruptibly 版本
try {
    while (!Thread.currentThread().isInterrupted()) {
        doWork();                       // 工作循环也要响应中断
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
} finally {
    lock.unlock();
}
1
2
3
4
5
6
7
8
9
10
11

# 2.4 tryLock 超时的正确用法

if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {     // ★ 限时 500ms
    try {
        doWork();
    } finally { lock.unlock(); }
} else {
    // ★ 拿不到锁的降级逻辑(如返回缓存、走 fallback)
    return fallback();
}
1
2
3
4
5
6
7
8

两个常见错误:

// ❌ 错误 1:忘记判断返回值
lock.tryLock(500, TimeUnit.MILLISECONDS);    // ← 没接收返回值,无论拿没拿到都执行后续!
doWork();
lock.unlock();

// ❌ 错误 2:try 写在外面
lock.tryLock(500, TimeUnit.MILLISECONDS);
try { doWork(); } 
finally { lock.unlock(); }                   // ← 没拿到锁也 unlock 会抛 IllegalMonitorStateException
1
2
3
4
5
6
7
8
9

正确范式:返回值必须接收,try 必须在判断之内——这是死规矩。

# 2.5 Condition 多条件队列高级用法

36 篇 §7 讲过 Condition 双队列结构。这里给一个有界缓冲生产者消费者的工程级实现,对比 synchronized + wait/notifyAll 的差距:

public class BoundedBuffer<E> {
    private final ReentrantLock lock     = new ReentrantLock();
    private final Condition    notFull  = lock.newCondition();    // ★ 等"不满"
    private final Condition    notEmpty = lock.newCondition();    // ★ 等"不空"
    private final Object[] items;
    private int putIdx, takeIdx, count;

    public BoundedBuffer(int capacity) { items = new Object[capacity]; }

    public void put(E x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) notFull.await();      // 等"不满"
            items[putIdx] = x;
            if (++putIdx == items.length) putIdx = 0;
            ++count;
            notEmpty.signal();                                   // ★ 精准唤醒消费者
        } finally { lock.unlock(); }
    }

    @SuppressWarnings("unchecked")
    public E take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) notEmpty.await();                 // 等"不空"
            E x = (E) items[takeIdx];
            if (++takeIdx == items.length) takeIdx = 0;
            --count;
            notFull.signal();                                    // ★ 精准唤醒生产者
            return x;
        } finally { lock.unlock(); }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

对比 synchronized 版——只能 notifyAll() 把生产者消费者全唤醒,所有人争一把锁后绝大多数又退回 wait——CPU 浪费在无效的"惊群"上。Condition 的多队列让等不同条件的线程互不打扰——这正是 ArrayBlockingQueue 的实现原型。

# 3. ReentrantReadWriteLock 源码剖析

# 3.1 高低位编码的状态布局

36 篇 §3.2、§8.2 已铺过——这里把状态机完整画清楚:

state (32 bit)
┌─────────────────────────────┬─────────────────────────────┐
│   高 16 位 sharedCount       │   低 16 位 exclusiveCount   │
│   (读锁 - 持有线程总次数)     │   (写锁 - 重入次数)         │
│   最多 65535                  │   最多 65535                │
└─────────────────────────────┴─────────────────────────────┘
   c >>> 16                       c & 0xFFFF
1
2
3
4
5
6
7

关键不变量:

  • sharedCount > 0 && exclusiveCount > 0 → 不可能(除非同一线程持写锁后又拿读锁,即"降级中间态")
  • sharedCount = 0 && exclusiveCount = 0 → 自由态
  • exclusiveCount > 0 → 必然由 exclusiveOwnerThread 独占

线程级读锁计数:state 高 16 位是所有读线程的总次数,但每个线程自己持读锁多少次需要 ThreadLocal<HoldCounter> 跟踪——这样才能支持"读锁也可重入"。

# 3.2 读锁获取链路

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;                                            // ① 别人持写锁,立即失败
    int r = sharedCount(c);
    if (!readerShouldBlock() &&                               // ② 是否该被阻塞(公平 vs 非公平)
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {             // ★ CAS 高 16 位 +1
        if (r == 0) {                                         // ③ 第一个读者:cache
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {                  // ④ 第一个读者重入
            firstReaderHoldCount++;
        } else {                                              // ⑤ 其他读者:用 ThreadLocal HoldCounter
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0) readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);                     // 回退到完整 CAS 自旋
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

精彩的"第一个读者优化":99% 场景就一个读者反复进出(如缓存),用 firstReader/firstReaderHoldCount 两个普通字段记录,避免 ThreadLocal 开销。这是 Doug Lea "为常见路径优化"的典型手法——卷一 09 篇 HashMap 的 firstNode、卷二 27 篇 Optional 缓存空对象都是同款思路。

# 3.3 写锁获取与重入

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 0) {
        // ★ 关键判断:要么没人持锁,要么必须是自己持写锁
        if (w == 0 || current != getExclusiveOwnerThread()) return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        setState(c + acquires);                              // 重入:低 16 位 +1
        return true;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))                // 自由态:CAS 抢
        return false;
    setExclusiveOwnerThread(current);
    return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

注意第一个 if 的精妙:c != 0 说明有人持锁,此时只有两种情况能继续——

  • w == 0 → 全是读者持锁 → 直接返 false(即使我自己也持读锁,也不能升写)
  • w != 0 && current == owner → 我自己持写锁 → 重入 +1

这一个判断同时禁止了"读升写"(§3.5)并支持了"写重入"——Doug Lea 用 5 行代码守住了整个一致性。

# 3.4 锁降级(写 → 读)的标准范式

降级合法:持写锁的线程在释放写锁前先获取读锁——这是 RWLock 唯一允许的"跨锁切换"。

public class CachedData {
    Object data;
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {
            rwl.readLock().unlock();                          // ★ 必须先放读锁
            rwl.writeLock().lock();
            try {
                if (!cacheValid) {                            // ★ 双重检查
                    data = ...
                    cacheValid = true;
                }
                rwl.readLock().lock();                        // ★ 持写锁时拿读锁(降级中间态)
            } finally {
                rwl.writeLock().unlock();                     // ★ 释放写锁,仅留读锁
            }
        }
        try {
            use(data);
        } finally { rwl.readLock().unlock(); }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

降级的核心目的:让该线程能继续以读模式访问,但期间禁止其他写者插入——保证"我刚写的数据自己能立刻读到"的强一致性。这是 ConcurrentHashMap 等并发容器的内部实现常用招式。

# 3.5 锁升级(读 → 写)为什么死锁

线程 A 持读锁 → 想升写 → 等所有读锁释放 → park
线程 B 持读锁 → 想升写 → 等所有读锁释放 → park
A 等 B 释放,B 等 A 释放 → 永久死锁 ⚠️
1
2
3

§3.3 的源码 if 直接禁止了这条路——读锁存在时(w == 0 但 sharedCount > 0),tryAcquire 直接返 false。正确做法:

// ❌ 错误:直接升级
rwLock.readLock().lock();
try {
    if (needWrite) rwLock.writeLock().lock();   // 死锁!
} finally { rwLock.readLock().unlock(); }

// ✅ 正确:先释放读锁,再抢写锁
rwLock.readLock().lock();
boolean needWrite = checkNeedWrite();
rwLock.readLock().unlock();                     // ★ 必须先放读锁

if (needWrite) {
    rwLock.writeLock().lock();
    try {
        if (stillNeedWrite()) doWrite();        // ★ 写锁内重新检查(可能其他线程已写)
    } finally { rwLock.writeLock().unlock(); }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

§1.2 的事故就是踩了这个坑——线上无脑升级最终演变为全线程 park。最优雅的解法在 §4.5——StampedLock 提供了 tryConvertToWriteLock 原子转换。

# 3.6 RWLock 的"写饥饿"难题

非公平模式下,读锁可以无限插队——只要有读者持锁,新来的读者直接获取,写者一直在队里等。极端场景下写者可能永不被唤醒。

// AQS 内部 readerShouldBlock 实现(非公平)
final boolean readerShouldBlock() {
    return apparentlyFirstQueuedIsExclusive();   // ★ 仅当队首是写者时才让读者排队
}
1
2
3
4

这是一个有限度的让步——队首是写者时新读者必须排队,避免完全饿死写者;但队中间还有写者时仍可能被无限插队。生产建议:高并发写场景必须用公平 RWLock 或换用 StampedLock。

# 4. StampedLock 乐观读革命

# 4.1 三种锁模式

JDK 8 引入的 StampedLock 不基于 AQS——它自己实现了一套队列,提供三种模式:

模式 API 性质 用途
写锁 writeLock() / unlockWrite(stamp) 独占 等价 RWLock 写锁
悲观读 readLock() / unlockRead(stamp) 共享 等价 RWLock 读锁
乐观读 tryOptimisticRead() / validate(stamp) 无锁 革命性创新 ⚡

核心创新:乐观读不上锁——直接读数据,事后用 validate(stamp) 验证读期间没有写入。无竞争的读不需要任何 CAS、不需要任何同步——这才是读多场景性能爆杀 RWLock 的根本原因。

# 4.2 stamp 戳的设计

每次成功获取(写/悲观读/乐观读)都返回一个 long 类型的 stamp——它编码了:

  • 当前的"版本号"(每次写锁释放时 +1)
  • 锁模式标志位
// 内部状态字段
private transient volatile long state;          // long = 64 bit,不再是 int
private static final long WBIT  = 1L << 7;      // 写位(state 第 7 位)
private static final long RBITS = WBIT - 1L;    // 读计数低 7 位
private static final long RFULL = RBITS - 1;
1
2
3
4
5

为什么用 long:32 位 int 不够装"版本号 + 模式 + 读计数",必须 64 位才能在一个原子变量里编码全部信息(再次回扣 36 篇 §3 "状态压缩"哲学)。

# 4.3 乐观读的标准范式

public class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    public double distanceFromOrigin() {
        long stamp = sl.tryOptimisticRead();         // ① 拿乐观戳(无锁!)
        double curX = x, curY = y;                   // ② 直接读字段
        if (!sl.validate(stamp)) {                   // ③ 验证期间没人写
            stamp = sl.readLock();                   // ④ 失败:升级悲观读
            try {
                curX = x; curY = y;
            } finally { sl.unlockRead(stamp); }
        }
        return Math.sqrt(curX * curX + curY * curY);
    }

    public void move(double dx, double dy) {
        long stamp = sl.writeLock();
        try { x += dx; y += dy; }
        finally { sl.unlockWrite(stamp); }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

乐观读三步曲:

  1. tryOptimisticRead() —— 仅仅读一下当前 state(一次 volatile read),不做任何 CAS、不入队
  2. 拷贝数据到本地变量(避免读期间字段被改)
  3. validate(stamp) —— 再读一次 state 与原 stamp 对比,没人写过就是有效;写过了走悲观读 fallback

性能本质:无写竞争时,读者的开销 = 2 次 volatile read——这跟"自由读裸字段"几乎无差别。

# 4.4 validate 与内存屏障

public boolean validate(long stamp) {
    VarHandle.acquireFence();                       // ★ 关键的内存屏障
    return (stamp & SBITS) == (state & SBITS);
}
1
2
3
4

为什么需要 acquireFence:乐观读中先读字段后 validate——必须保证字段读取不被重排到 validate 之后,否则可能验证通过但读到旧值。acquireFence 是 LoadLoad + LoadStore 屏障(卷四 31 篇 VarHandle),确保后续操作不被前移。

这是无锁编程的精髓:用最便宜的内存屏障代替昂贵的锁。这也是 StampedLock 不能基于 AQS 的原因——AQS 没暴露这种细粒度屏障 API,只能自己撸。

# 4.5 锁转换 API(tryConvertToWriteLock)

StampedLock 提供了原子化的锁模式切换——这是它解决 RWLock 升级死锁的杀手锏:

public void moveIfAtOrigin(double newX, double newY) {
    long stamp = sl.readLock();                     // 先持读锁
    try {
        while (x == 0.0 && y == 0.0) {
            long ws = sl.tryConvertToWriteLock(stamp);   // ★ 原子升级
            if (ws != 0L) {
                stamp = ws;
                x = newX; y = newY;
                break;
            } else {
                sl.unlockRead(stamp);
                stamp = sl.writeLock();              // ★ 退回先释放再抢
            }
        }
    } finally { sl.unlock(stamp); }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

tryConvertToWriteLock 的精妙:

  • 当前 stamp 是写锁 → 直接返回(已经是写锁)
  • 当前是读锁且只有自己持读锁 → CAS 升级为写锁,返回新 stamp
  • 否则(多个读者)→ 返回 0L 失败

唯一支持锁升级的 Lock 实现——但仍要做好 fallback 处理。

# 4.6 StampedLock 的四大坑

⚠️ 坑 1:不可重入——同一个线程再次拿同种锁会死锁。RWLock 写锁可重入,StampedLock 写锁不可重入。

⚠️ 坑 2:不支持 Condition——没有 newCondition() API。需要等待/通知场景必须用 ReentrantLock。

⚠️ 坑 3:不响应中断(默认)——writeLock() / readLock() 不响应中断,要用 writeLockInterruptibly() / readLockInterruptibly()。

⚠️ 坑 4:乐观读必须配字段拷贝 + validate——直接在乐观读区间内调用其他方法、抛异常都可能读到不一致状态。乐观读区间必须是"快照式"的纯字段读取,不能有副作用。

// ❌ 错误用法:乐观读期间做复杂操作
long stamp = sl.tryOptimisticRead();
if (cache.get(key).isExpired()) {     // ← 期间可能读到撕裂状态,抛 NPE
    refresh();
}
sl.validate(stamp);

// ✅ 正确用法:先拷贝、再 validate、再用拷贝
long stamp = sl.tryOptimisticRead();
Snapshot snap = new Snapshot(field1, field2);
if (!sl.validate(stamp)) {
    stamp = sl.readLock();
    try { snap = new Snapshot(field1, field2); }
    finally { sl.unlockRead(stamp); }
}
useSnapshot(snap);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 5. 三剑客性能横评

# 5.1 测试场景设计

环境:32 核 / 64 线程 / JDK 21 / 临界区耗时 ~ 1μs
对比:synchronized / ReentrantLock / RWLock / StampedLock(乐观读)
读写比:95:5 / 50:50 / 5:95
指标:吞吐 (ops/s) + P99 延迟 (μs)
1
2
3
4

# 5.2 读多写少(95:5)

                    吞吐 (M ops/s)    P99 (μs)
synchronized            2.1            420
ReentrantLock           3.8            280
RWLock                  9.5             95
StampedLock(乐观)      32.0             18    ★★★ 碾压 ★★★
1
2
3
4
5

StampedLock 暴打的根因:乐观读路径完全无锁——95% 的读操作只做 2 次 volatile read。RWLock 即使读不互斥,每次读还是要 CAS state 高 16 位 +1。

# 5.3 读写均衡(50:50)

                    吞吐 (M ops/s)    P99 (μs)
synchronized            2.0            450
ReentrantLock           3.2            350
RWLock                  3.5            340     ← 读写各半时优势消失
StampedLock(乐观)       4.8            220
1
2
3
4
5

关键观察:50:50 时 RWLock 几乎没有优势——大量乐观读会因写者频繁修改而 validate 失败、退化为悲观读。StampedLock 仍领先但优势收窄。

# 5.4 写多读少(5:95)

                    吞吐 (M ops/s)    P99 (μs)
synchronized            1.8            500
ReentrantLock           2.8            380
RWLock                  2.5            420     ← 反而比 ReentrantLock 慢
StampedLock(乐观)       2.6            390     ← 乐观读 95% 失败,全走悲观
1
2
3
4
5

关键观察:写多场景下 RWLock 反而比 ReentrantLock 慢——原因是 RWLock 的内部状态管理比 ReentrantLock 更复杂(高低位拆分 + ThreadLocal HoldCounter),没有读多收益时这些开销纯亏。

# 5.5 横评结论与选型决策树

是否有竞争?
├── 几乎无竞争
│    └── synchronized(JDK 6 偏向锁/轻量锁红利)
│
└── 有竞争
     │
     读写比例如何?
     ├── 写为主(写 ≥ 30%)
     │    └── ReentrantLock(默认非公平)
     │
     ├── 读多写少(读 ≥ 70%)
     │    │
     │    是否需要 Condition / 中断 / 重入?
     │    ├── 是 → ReentrantReadWriteLock
     │    └── 否 → StampedLock(乐观读 ⚡)
     │
     └── 写极少(读 ≥ 95%)
          └── StampedLock(吞吐爆杀 RWLock 3 倍 +)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

额外维度:

  • 需要多条件队列 → 必选 ReentrantLock(StampedLock 不支持)
  • 需要锁可重入 → 不能用 StampedLock
  • 强一致性写后读 → 必须 RWLock 锁降级范式(§3.4)

# 6. Loom 时代的 Lock 选型

# 6.1 synchronized 的 Pinning 痛点

JDK 21 虚拟线程(41 篇会展开)下,synchronized 会把虚拟线程"钉"在 carrier 线程上——其他虚拟线程无法复用该 carrier,调度优势完全丧失:

// ⚠️ 虚拟线程下的"钉死"陷阱
synchronized (lock) {
    Thread.sleep(1000);    // ← 虚拟线程被 pin,carrier 无法切换
}
1
2
3
4

根因:synchronized 是 JVM 内置 monitor,进入 monitor 的栈帧是 C++ 物理栈帧——Loom 没法把它从 carrier 上"卸下来"挂起。

# 6.2 Lock 三剑客的虚拟线程友好性

              是否 Pin carrier   原因
synchronized       是 ❌           C++ monitor 物理栈帧
ReentrantLock      否 ✅           AQS 基于 LockSupport.park(支持虚拟线程友好挂起)
RWLock             否 ✅           同上
StampedLock        否 ✅           内部也用 LockSupport.park
1
2
3
4
5

Loom 设计准则:所有阻塞点都走 LockSupport.park——park 在虚拟线程上不是 OS 挂起,而是切回调度器、yield carrier。三剑客早早就埋好了这个伏笔——这是 36 篇 §8.5 提到的 "Doug Lea 早早给虚拟线程留好了门"。

# 6.3 JDK 24 的 synchronized 解 Pin

JDK 24(2025-03 发布)推进 JEP 491: Synchronize Virtual Threads without Pinning (opens new window)——重写了 monitor 实现,synchronized 不再 Pin!

JDK 21: synchronized → Pin
JDK 24: synchronized → 不再 Pin(默认开启)
1
2

这是否意味着 Lock 三剑客失去优势?并没有——三剑客在 Loom 下仍是首选:

  • synchronized 解 Pin 只在 monitor 不抢占 native 栈帧时生效——一旦 JNI 调用还会 Pin
  • ReentrantLock 仍提供 synchronized 没有的能力(中断、超时、Condition、公平)
  • StampedLock 的乐观读是任何 synchronized 优化都比不上的零开销路径

41 篇会再深入 Loom 内部机制 + Pinning 完整诊断方案。

# 7. 工程实战避坑

# 7.1 lock 必须配 try-finally

// ❌ 致命错误:锁泄漏
lock.lock();
doWork();        // 抛异常 → unlock 永远不执行 → 后续所有线程死等
lock.unlock();

// ✅ 标准范式
lock.lock();
try {
    doWork();
} finally {
    lock.unlock();
}
1
2
3
4
5
6
7
8
9
10
11
12

lock() 必须在 try 之外,unlock 必须在 finally——这是死规矩。tryLock 配套是 if (lock.tryLock()) { try {...} finally {lock.unlock();} }。

# 7.2 锁顺序与死锁四要素

死锁四要素(操作系统经典):互斥、占有且等待、不可剥夺、循环等待。打破任意一条即可避免:

// ❌ 死锁经典示范
Thread1: lock(A) → lock(B)
Thread2: lock(B) → lock(A)        ← 循环等待

// ✅ 全局锁顺序:永远按 hashCode 顺序加锁
void transfer(Account from, Account to, int amount) {
    Account first  = from.id < to.id ? from : to;
    Account second = from.id < to.id ? to : from;
    first.lock.lock();
    try {
        second.lock.lock();
        try { /* transfer */ }
        finally { second.lock.unlock(); }
    } finally { first.lock.unlock(); }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 7.3 锁粒度的"先粗后细"原则

开发阶段:先用粗粒度锁(一把大锁罩全场)→ 保证正确性
压测发现瓶颈 → 拆分锁(按数据分片、按读写分离)
1
2

典型案例:

  • HashMap → Hashtable(一把大锁)→ ConcurrentHashMap(分段锁 → CAS+synchronized 节点锁)
  • 数据库行锁 → 表锁 → 库锁(粒度从细到粗,并发度从高到低)

反例:上来就拆 1000 个锁——大概率某次 bug 修复时漏掉一把锁,引入幽灵 bug。先正确,再快。

# 7.4 监控埋点:getQueueLength 与 isLocked

ReentrantLock 提供丰富的监控 API(StampedLock 没有,是它的另一个短板):

ReentrantLock lock = new ReentrantLock();

lock.getQueueLength()             // 当前等待队列长度
lock.hasQueuedThreads()           // 是否有人在等
lock.hasQueuedThread(thread)      // 指定线程是否在等
lock.isLocked()                   // 是否被持有
lock.isHeldByCurrentThread()      // 是否当前线程持有
lock.getHoldCount()               // 当前线程的重入次数

// Condition 监控
lock.hasWaiters(condition)
lock.getWaitQueueLength(condition)
1
2
3
4
5
6
7
8
9
10
11
12

生产埋点范式:

@Scheduled(fixedDelay = 1000)
public void monitor() {
    int queueLen = lock.getQueueLength();
    if (queueLen > THRESHOLD) {
        log.warn("Lock contention: {} threads waiting", queueLen);
        // 报警 / 触发熔断 / 降级
    }
    metrics.gauge("lock.queue.length", queueLen);
}
1
2
3
4
5
6
7
8
9

# 8. 综合回扣与设计哲学

# 8.1 案例真相揭晓

① §1.1 缓存读写之痛的真相:synchronized 把 800 个读线程串行化是性能杀手;RWLock 让读并行但写者饥饿;StampedLock 乐观读才是正解——95% 读场景下 P99 从 480ms 降到 12ms(40 倍提升)。根治:按读写比例选锁,超读多场景(≥95%)必上 StampedLock 乐观读。

② §1.2 RWLock 升级死锁的真相:读锁可被多线程持有,所以"等所有读释放"在多读者场景下永远等不完。根治:要么"先释放读锁再抢写锁"两阶段加锁,要么直接用 StampedLock.tryConvertToWriteLock 原子转换。

③ 6 大追问全部作答:

追问 答案 章节
① ReentrantLock 强在哪 中断、超时、公平、多 Condition、tryLock 五大特性 §2
② RWLock 高低位实现 32 位 state 拆 16+16,第一个读者优化用 firstReader §3.1-3.2
③ StampedLock 乐观读凭什么暴涨 无锁路径 = 2 次 volatile read,无 CAS、无入队 §4.3
④ 三剑客性能曲线 读多 StampedLock 完胜,写多 ReentrantLock 反而最快 §5
⑤ Loom 下 synchronized 是否废 JDK 24 解 Pin 但仍逊于三剑客功能 §6
⑥ 不死锁不饿死可监控的代码 try-finally + 全局锁序 + getQueueLength 埋点 §7

# 8.2 三剑客设计哲学

1. "状态压缩"是并发数据结构的命脉:RWLock 一个 int 装下读+写两种状态、StampedLock 一个 long 装下版本号+模式+读计数——这种"位编码"省下了多字段同步的不一致性。回扣 36 篇 §3 "状态压缩"哲学,也呼应卷一 09 篇 ConcurrentHashMap 的 sizeCtl 多义编码、卷一 10 篇对象头 markword。这给我们的启示:并发设计中,能用一个原子变量编码的状态绝不用两个——不是为了省内存,而是为了省"原子性"。

2. "乐观并发"是高读场景的终极答案:StampedLock 的乐观读用"读 → 拷贝 → 验证"三步走代替了"加锁 → 读 → 解锁",本质是把同步代价从必然付出改为按需付出——只有真正发生写竞争时才退化为悲观读。这种思路在数据库(MVCC、乐观锁版本号)、Git(提交版本号检测冲突)、CAS(无锁原语)中无处不在。乐观并发的前提是"冲突罕见"——读多场景成立,写多场景反成累赘(§5.4)。

3. "工具不是越多越好,而是要匹配场景":synchronized 简单可靠、ReentrantLock 功能全、RWLock 读写分离、StampedLock 极致性能——没有最好的锁,只有最合适的锁。架构师的价值不是"用最炫的工具",而是"用最贴合业务读写比的工具"。这也是为什么 Java 不淘汰 synchronized——简单场景下它的简洁和稳定无可替代。

# 8.3 速查表与下一篇预告

API 速查:

ReentrantLock                       ReentrantReadWriteLock
─ lock() / unlock()                 ─ readLock() / writeLock()
─ lockInterruptibly()               ─ 写降读:先持写,再获读,再释写
─ tryLock(time, unit)               ─ 不支持读升写
─ newCondition()                    ─ 支持 Condition (写锁)
─ isFair() / getQueueLength()       ─ 高 16 位读,低 16 位写

StampedLock                         选型口诀
─ tryOptimisticRead() + validate()  ─ 无竞争 → synchronized
─ readLock() / unlockRead()         ─ 写为主 → ReentrantLock
─ writeLock() / unlockWrite()       ─ 读多 ≥ 70% → RWLock
─ tryConvertToWriteLock()           ─ 读多 ≥ 95% → StampedLock 乐观读
─ ⚠️ 不可重入、无 Condition           ─ 多 Condition → ReentrantLock
1
2
3
4
5
6
7
8
9
10
11
12
13

死规矩:

1. lock 必须在 try 外,unlock 必须在 finally
2. tryLock 必须接收返回值,try 必须在 if 内
3. RWLock 永远不要"读升写",只允许"写降读"
4. StampedLock 乐观读区间内必须先拷贝字段再 validate
5. 全局锁顺序统一(按 hashCode 或 id),消除循环等待
6. 生产环境给所有锁埋监控点(getQueueLength + 报警)
1
2
3
4
5
6

🎯 下一篇预告:第 38 篇《CAS、Atomic、Unsafe 与 VarHandle》——本篇站在 AQS/Lock 这一层讲"用户态阻塞同步",下一篇沉到更底层的无锁原语:CAS 怎么由 CPU 指令保证原子性、Atomic 全家桶(AtomicInteger/AtomicReference/LongAdder)的实现差异、Unsafe 那些"危险但强大"的 API、JDK 9 之后为什么用 VarHandle 取代 Unsafe,以及 LongAdder 分段累加为什么比 AtomicLong 在高并发下吞吐高 10 倍。无锁编程的物理基础一次讲透。

上次更新: 2026/06/10, 11:13:41
AQS同步框架源码
CAS和Atomic深入分析

← AQS同步框架源码 CAS和Atomic深入分析→

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