# 彻底搞懂 ReentrantLock 与 tryLock:从秒杀实战到 AQS 独占模式源码剖析

彻底搞懂 ReentrantLock 与 tryLock:从秒杀实战到 AQS 独占模式源码剖析

在 Java 高并发编程中,synchronized 是我们最熟悉的互斥手段。然而在面对"高并发秒杀"这类典型场景时,synchronized 的缺点也会被无限放大:抢不到锁的线程只能死等。当百万请求瞬间涌入,大量线程堆积阻塞在锁上,极易引发上下文切换风暴,甚至直接拖垮整个系统。

业务真正需要的,往往是"尝试获取 + 超时放弃 + 灵活响应"。JDK 提供的显式锁 ReentrantLock 及其 tryLock(timeout) 机制,正是为了将锁的控权重新交还给程序员。

为什么需要 ReentrantLock?

ReentrantLockjava.util.concurrent.locks.Lock 接口的实现,底层完全基于 AQS(AbstractQueuedSynchronizer)的独占模式构建。

它与关键字 synchronized 的核心功能契合,但赋予了更丰富的控锁能力:

API 方法 核心语义 关键特性
lock() 阻塞式获取锁 抢不到一直等,行为与 synchronized 一致
tryLock() 非阻塞尝试 立即返回 true/false完全不等待(且忽略公平性规则)
tryLock(time, unit) 限时尝试 指定时间内抢到返回 true,超时返回 false,可响应中断
unlock() 释放锁 必须放在 finally,否则必定导致锁泄漏
lockInterruptibly() 可中断获取 在等待锁的过程中允许响应 Thread.interrupt()

可重入性(Reentrant) :同一线程可多次调用 lock(),内部计数器 state 累加;每次 lock() 必须对应一次 unlock(),当计数归零时锁才算真正释放。

实战演练:tryLock 解决秒杀限时抢购

以"100 万线程争抢 100 件库存"为例,核心思路就是:LOCK.tryLock(1000, TimeUnit.MILLISECONDS)------给线程 1 秒钟的缓冲期,抢得到就扣减,抢不到直接超时放弃,绝不卡死线程池。

Java

java 复制代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class SecKillDemo {
    private static final ReentrantLock LOCK = new ReentrantLock();
    private static int stock = 100;

    public static void main(String[] args) {
        for (int i = 0; i < 100000; i++) {
            new Thread(() -> {
                SecSkillService service = new SecSkillService();
                if (service.secSkill()) {
                    System.out.println(Thread.currentThread().getName() + " 抢购成功,剩余库存:" + stock);
                } else {
                    System.err.println(Thread.currentThread().getName() + " 抢购失败(系统繁忙或已售罄)");
                }
            }).start();
        }
    }

    static class SecSkillService {
        public boolean secSkill() {
            try {
                // 限时尝试:1秒内抢到锁返回 true,超时返回 false,不阻塞线程
                if (LOCK.tryLock(1000, TimeUnit.MILLISECONDS)) {
                    try {
                        if (stock > 0) {
                            Thread.sleep(10); // 模拟扣减库存的业务耗时
                            stock--;
                            return true;
                        }
                        return false; // 库存不足
                    } finally {
                        LOCK.unlock(); // ★ 只要加锁成功,必须在 finally 中释放
                    }
                } else {
                    return false; // 1秒内没抢到锁,直接超时放弃
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
        }
    }
}

执行流转逻辑

scss 复制代码
线程调用 secSkill()
   │
   ├──> tryLock(1000ms)
          ├─ [抢锁成功] ──> 判断库存 ──> 扣减 ──> finally { unlock() } ──> 返回 true
          └─ [超时未抢到] ──────────────────────────────────────────> 返回 false

最佳实践扩展 :在生产环境中,可将布尔返回值重构成状态枚举 SecKillStatus { SUCCESS, STOCK_EMPTY, LOCK_TIMEOUT, INTERRUPTED },方便前端区分是"已售罄"还是"系统繁忙请求超时"。

撬开底层:AQS 独占模式如何运作?

ReentrantLock 本身并不直接操作线程排队,它的加解锁完全委派给内部类 Sync(继承自 AbstractQueuedSynchronizer)。

1. AQS 核心三要素

  • volatile int state :0 表示锁空闲;大于 0 表示锁已被占用,数值代表重入次数

  • exclusiveOwnerThread:记录当前持有锁的线程,用来判断"当前线程是否在重入"。

  • CLH 虚拟双向队列 :抢锁失败的线程封装为 Node 节点,在此队列中挂起等待。

2. 核心决策:tryAcquire 抢锁三步法

当执行 lock()tryLock() 时,底层的 tryAcquire 会经历三步决策:

scss 复制代码
                 tryAcquire(1) 决策流程
                           │
             ┌─────────────┴─────────────┐
             ▼                           ▼
    [① state > 0 且 holder == self]   [② state == 0 (锁空闲)]
             │                           │
      (满足重入条件)                (尝试 CAS 抢锁)
             │                           │
             ▼                           ├─ CAS 成功 ──> 设 owner,返回 true
      state + 1,返回 true               │
                                         └─ CAS 失败 ──> 进入下一步
                                                 │
                                                 ▼
                                        [③ 入 CLH 队列 park()]
                                         (挂起等待前驱唤醒)
步骤 判断条件 对应动作 最终结果
① 重入判断 state > 0owner == 当前线程 state + 1 成功获取,流程直接结束
② CAS 抢锁 state == 0(锁目前空闲) CAS 将 state 从 0 改为 1 并记录 owner 成功则结束;失败则走向步骤 ③
③ 入队挂起 ① 和 ② 均不成立 封装入 CLH 队列,调用 LockSupport.park() 线程挂起,等待前驱节点唤醒

3. CLH 队列中的接力与挂起

线程抢锁失败入队后,并不是简单地死循环,而是遵循严格的接力机制:

lua 复制代码
  +------+  next   +------------+  next   +------------+
  | Head | ------> | head.next  | ------> |   Node B   | ...
  | (哨兵) | <------ | (队头等待者) | <------ |  (普通等待) |
  +------+  prev   +------------+  prev   +------------+
  1. 哨兵 Head 节点:队列头节点是一个虚节点(哨兵),代表"当前正持有锁或刚释放锁的线程"。

  2. 队头争抢权 :只有 head.next(队头第一个有效等待节点)才有资格在被唤醒后调用 tryAcquire 抢锁。

  3. 单向唤醒接力 :当持锁线程调用 unlock() 时,tryReleasestate 减为 0,随后唤醒 head 的后继节点(head.next)。被唤醒的线程重新检查前驱是否为 head,若是则尝试 CAS 拿锁,成功后把自己提升为新的 head

核心差异对比

1. 公平锁 vs 非公平锁

Java

ini 复制代码
// 默认非公平锁
ReentrantLock nonFairLock = new ReentrantLock(); 
// 公平锁
ReentrantLock fairLock = new ReentrantLock(true); 

两者的加锁区别仅在于 tryAcquire 中的一行关键代码

scss 复制代码
[非公平锁 NonfairSync]:
  抢锁时直接 CAS(0->1) 插队!插队失败才去检查重入和入队。

[公平锁 FairSync]:
  抢锁前先调用 hasQueuedPredecessors()!
  └─ 检查 CLH 队列中是否有排在自己前面的线程。
      ├─ 有人排队:放弃抢锁,乖乖入队。
      └─ 无人排队:才允许 CAS 抢锁。
维度 非公平锁(默认) 公平锁
抢锁行为 新线程刚来就尝试插队抢锁 严格遵守 FIFO,排队优先
吞吐量 更高(减少了线程唤醒与上下文切换开销) 较低
适用场景 追求高并发、高吞吐的绝大多数业务 对排队顺序敏感、防线程"饥饿"的场景

关键特例 :无参的 tryLock() 无论底层是公平锁还是非公平锁,都会直接插队 !只有带超时参数的 tryLock(time, unit) 才会遵循公平锁的排队规则。

2. ReentrantLock vs synchronized

比较维度 synchronized ReentrantLock
实现层级 JVM 层面(monitorenter/monitorexit JDK API 层面(AQS + CAS)
灵活度 隐式自动加解锁,无法中断或超时 显式手动加解锁,支持 tryLock 与中断
公平性 仅支持非公平 支持公平锁与非公平锁切换
锁升级 支持偏向锁/轻量级锁/重量级锁升级 无锁升级概念(依赖 AQS 队列排队)

避坑指南

  1. 忘写 unlock() 导致锁泄漏ReentrantLock 没有 JVM 的自动释放机制。铁律lock()tryLock() == true 之后,必须紧跟 try { ... } finally { LOCK.unlock(); }

  2. finally 中无条件 unlock() :如果 tryLock() 返回 false(未拿到锁),绝对不能在 finally 中调用 unlock(),否则会抛出 IllegalMonitorStateException

  3. 重入次数不匹配 :调用了 2 次 lock(),就必须调用 2 次 unlock()。若释放次数不够,state 无法归零,锁将永远无法被其他线程获取。

  4. 误解 tryLock() 的公平性 :无参 tryLock() 不会遵守公平锁规则,如需公平超时抢锁,请务必使用 tryLock(0, TimeUnit.SECONDS)

自测思考

  1. tryLock()tryLock(1, TimeUnit.SECONDS) 在公平锁模式下的行为有何不同?

  2. CLH 队列中的 head 节点为什么是一个不带线程引用的"哨兵节点"?

  3. 如果一个线程在等待 tryLock(5, TimeUnit.SECONDS) 的过程中被中断,会发生什么?

相关推荐
啷里格啷1 小时前
Linux进程管理完全指南:从基础到云原生编排
后端·架构
洛阳泰山1 小时前
AI 应用层被 Python 卷成红海,为什么我偏要用 Java 造一个 RAG + 工作流引擎?
java·人工智能·后端
Wz_z_z_z1 小时前
SpringBoot Actuator 泄露挖掘实战:从 /env 到 heapdump
后端
9i编程1 小时前
工具是编程的铠甲(下篇):从文件对比、全文搜索到数据库设计
后端·openai·ai编程
思考着亮1 小时前
4.Redis 核心概念与实战深度
后端
Java编程爱好者2 小时前
Reactor 网络模型演进:图解 7 种 Reactor 网络模型
后端
YuePeng2 小时前
一个注解起家,25 个模块收尾:Erupt 的生态版图长这样
后端·github
睡觉时不困4422 小时前
7.从底层讲清楚Git仓库
后端
神奇小汤圆2 小时前
解放双手!SpringBoot 6种自动填充公共字的手段,这些代码真没必要手写!
后端