当前位置: 首页 > news >正文

Java并发编程:ReentrantLock与AQS原理剖析

前言

在Java并发编程中,ReentrantLock是一个非常重要的可重入互斥锁,它比synchronized提供了更灵活的锁机制:支持公平/非公平模式、可响应中断、超时等待、多条件变量等。

但真正让ReentrantLock强大的,是其底层依赖的AbstractQueuedSynchronizer(AQS)框架。AQS是JUC(java.util.concurrent)包的基石,CountDownLatchSemaphoreReentrantReadWriteLock等同步组件都基于它构建。

本文将逐行解析源码,彻底讲清楚:

  • AQS的核心数据结构(state、CLH队列、Node)

  • 非公平锁的完整加锁/解锁流程

  • 公平锁的实现原理及与非公平锁的区别

  • 同步队列的入队、阻塞、唤醒机制

  • 重入锁的实现原理


一、AQS核心架构

1.1 AQS是什么?

AbstractQueuedSynchronizer是一个抽象类,它提供了一个FIFO队列来管理等待锁的线程,并定义了一个int类型的state作为同步状态。

子类只需要实现tryAcquiretryRelease等钩子方法,即可构造出自己的同步器。

1.2 核心属性

// AbstractQueuedSynchronizer 中的核心属性 // 同步状态,volatile保证可见性 // 在ReentrantLock中:state=0表示锁空闲,state>0表示锁被持有(值表示重入次数) private volatile int state; // 同步队列的头指针(懒加载,初始化时为空) private transient volatile Node head; // 同步队列的尾指针 private transient volatile Node tail;
// AbstractOwnableSynchronizer(AQS的父类)中的属性 // 记录当前持有锁的线程(独占模式下使用) private transient Thread exclusiveOwnerThread;

1.3 Node节点结构

同步队列中的每个节点都是一个Node对象:

static final class Node { // 节点等待模式 static final Node SHARED = new Node(); // 共享模式 static final Node EXCLUSIVE = null; // 独占模式 // 等待状态常量 static final int CANCELLED = 1; // 线程已取消等待 static final int SIGNAL = -1; // 当前节点释放锁后需要唤醒后继节点 static final int CONDITION = -2; // 在条件队列中等待 static final int PROPAGATE = -3; // 共享模式下需要传播唤醒 // 节点状态(重要:-1 SIGNAL,1 CANCELLED,0 初始状态) volatile int waitStatus; // 双向链表指针 volatile Node prev; // 前驱节点 volatile Node next; // 后继节点 // 该节点封装的线程 volatile Thread thread; // 指向条件队列的下一个节点(Condition相关,本文暂不详述) Node nextWaiter; }

1.4 同步队列结构图

重要特性

  1. 队列是FIFO(先进先出)的

  2. 头节点是一个哨兵节点,一般不关联具体的线程(或关联当前持有锁的线程)

  3. 每个节点封装一个等待的线程

  4. 节点状态SIGNAL表示:前驱节点释放锁后会唤醒我


二、非公平锁完整源码解析

2.1 加锁入口:lock()

// ReentrantLock 构造方法 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } // 调用 lock() 时,实际执行的是 sync.lock() public void lock() { sync.lock(); }

NonfairSync.lock()实现:

final void lock() { // 【第一次插队机会】快速CAS尝试获取锁 // 如果锁空闲(state=0),则直接抢锁成功 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); // 抢锁失败,进入完整获取流程 }

为什么叫非公平?新来的线程可以不排队,直接尝试抢锁。如果抢成功了,队列中等待的线程只能继续等。

2.2 acquire:AQS模板方法

public final void acquire(int arg) { // tryAcquire:尝试获取锁(包含重入逻辑) // addWaiter:获取失败,将当前线程封装成节点加入队列 // acquireQueued:在队列中自旋等待 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); // 如果等待过程中被中断过,补上中断标记 }

2.3 tryAcquire:尝试获取锁

AQS中的钩子方法:

protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }

NonfairSync中的实现:

protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }

核心逻辑 nonfairTryAcquire

final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // 【情况1】锁空闲 if (c == 0) { // 【第二次插队机会】CAS获取锁 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 【情况2】锁已被当前线程持有 → 重入 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // 溢出保护 throw new Error("Maximum lock count exceeded"); setState(nextc); // 不需要CAS,因为只有当前线程才能执行到这里 return true; } // 【情况3】锁被其他线程持有 return false; }

重入锁的关键:同一个线程可以多次获取同一把锁,每次重入state加1,释放时需要释放相同次数。

2.4 addWaiter:入队操作

private Node addWaiter(Node mode) { // 创建新节点,封装当前线程,mode=null表示独占模式 Node node = new Node(Thread.currentThread(), mode); // 【快速尝试】如果队列已存在,尝试直接CAS追加到队尾 Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 【兜底方案】队列不存在或CAS失败,进入自旋入队 enq(node); return node; }

2.5 enq:自旋入队(线程安全)

private Node enq(final Node node) { for (;;) { // 自旋,直到入队成功 Node t = tail; if (t == null) { // 队列尚未初始化 // 创建哨兵节点作为头节点 if (compareAndSetHead(new Node())) tail = head; // 头尾指向同一个哨兵节点 } else { // 标准入队操作 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; // 返回前驱节点 } } } }

为什么要创建哨兵节点?有了哨兵节点,队列永远不会为空,简化了唤醒逻辑——每次从头节点的后继开始唤醒即可。

2.6 acquireQueued:排队等待

这是最核心的等待逻辑:

final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); // 获取前驱节点 // 【关键判断1】前驱是头节点 → 说明当前节点是队列中等待最久的 if (p == head && tryAcquire(arg)) { // 获取锁成功,将当前节点设置为新头节点 setHead(node); // 头节点会清空thread引用 p.next = null; // help GC failed = false; return interrupted; // 正常退出 } // 【关键判断2】获取锁失败,判断是否需要阻塞 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; // 记录中断状态 } } finally { if (failed) cancelAcquire(node); // 异常情况取消获取 } }

核心理解

  • 只有前驱是头节点的节点,才有资格尝试获取锁(保证FIFO)

  • 获取失败时,会阻塞自己,等待前驱节点唤醒

2.7 shouldParkAfterFailedAcquire:决定是否阻塞

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; // 【情况1】前驱状态已经是SIGNAL if (ws == Node.SIGNAL) // 放心阻塞,前驱释放锁时会唤醒我 return true; // 【情况2】前驱已取消等待 if (ws > 0) { // 跳过所有取消的节点,向前找到第一个有效节点 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } // 【情况3】前驱状态为0或PROPAGATE else { // 将前驱状态设置为SIGNAL // 注意:这里只是设置状态,返回false,外层会再尝试一次获取锁 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; // 当前不需要阻塞,外层会再循环一次 }

为什么设置SIGNAL后返回false?因为设置完成后,前驱节点可能刚好释放了锁,所以应该再给当前节点一次获取锁的机会。

2.8 parkAndCheckInterrupt:阻塞线程

private final boolean parkAndCheckInterrupt() { LockSupport.park(this); // 阻塞当前线程 return Thread.interrupted(); // 唤醒后返回中断状态并清除中断标记 }

LockSupport.park()会让线程进入WAITING状态,直到被unpark()唤醒。

2.9 加锁完整流程图

┌─────────────────────────────────────┐ │ lock() 调用 │ └─────────────────┬───────────────────┘ ▼ ┌─────────────────────────────────────┐ │ 快速CAS抢锁 (state=0→1)? │ └─────────────────┬───────────────────┘ 成功 │ │ 失败 ▼ ▼ ┌─────────────────┐ ┌─────────────────────────┐ │ 获取锁成功 │ │ tryAcquire尝试获取锁 │ │ 设置owner线程 │ │ (含重入逻辑) │ └─────────────────┘ └───────────┬─────────────┘ ▼ 成功 │ │ 失败 ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ 获取锁成功 │ │ addWaiter │ └─────────────────┘ │ 入队 │ └────────┬────────┘ ▼ ┌─────────────────┐ │ acquireQueued │ │ 自旋等待 │ └────────┬────────┘ ▼ ┌─────────────────┐ │ 前驱是头节点? │ └────────┬────────┘ 是 │ 否 ▼ ┌─────────────────┐ │ tryAcquire成功? │ └────────┬────────┘ 是 │ 否 ▼ ┌─────────────────┐ │ 设置新头节点 │ │ 返回 │ └─────────────────┘ │ 否 ▼ ┌─────────────────┐ │ shouldPark... │ │ 确保前驱SIGNAL │ └────────┬────────┘ ▼ ┌─────────────────┐ │ parkAndCheck... │ │ 阻塞线程 │ └────────┬────────┘ │ (唤醒后重新自旋) ◄──────────────┘

三、解锁流程完整解析

3.1 unlock入口

public void unlock() { sync.release(1); }

3.2 release:AQS释放模板

public final boolean release(int arg) { if (tryRelease(arg)) { // 尝试释放锁 Node h = head; // 头节点不为空且状态不为0(说明有需要唤醒的后继节点) if (h != null && h.waitStatus != 0) unparkSuccessor(h); // 唤醒后继节点 return true; } return false; }

3.3 tryRelease:释放锁(重入减1)

protected final boolean tryRelease(int releases) { int c = getState() - releases; // 安全检查:只有持有锁的线程才能释放 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { // 完全释放(重入计数归零) free = true; setExclusiveOwnerThread(null); } setState(c); // 更新state(不需要CAS,因为只有当前线程能执行这里) return free; }

关键点:重入锁的释放是分层的。每次unlock()只减少一次计数,只有计数归零时,锁才真正释放。

3.4 unparkSuccessor:唤醒后继线程

private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); // 清除SIGNAL状态 Node s = node.next; // 如果后继节点为空或已取消,从尾部向前找第一个有效节点 if (s == null || s.waitStatus > 0) { s = null; // 为什么要从后往前?因为并发环境下next指针可能不完整,但prev是可靠的 for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); // 唤醒线程 }

3.5 解锁流程图

┌─────────────────────────────────────┐ │ unlock() 调用 │ └─────────────────┬───────────────────┘ ▼ ┌─────────────────────────────────────┐ │ tryRelease: state减1 │ │ 判断是否完全释放 (c == 0)? │ └─────────────────┬───────────────────┘ 否 │ │ 是 ▼ ▼ ┌─────────────────┐ ┌─────────────────────────┐ │ 返回false │ │ 清空owner线程 │ │ 锁仍被持有 │ │ 返回true │ └─────────────────┘ └───────────┬─────────────┘ ▼ ┌─────────────────────────────┐ │ head != null && │ │ head.waitStatus != 0? │ └───────────┬─────────────────┘ 是 │ 否 ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ unparkSuccessor │ │ 直接返回 │ │ 唤醒后继节点 │ └─────────────────┘ └─────────────────┘

四、公平锁实现原理

4.1 公平锁与非公平锁的核心区别

公平锁与非公平锁的唯一区别在于:尝试获取锁时,是否检查队列中已有等待线程

特性非公平锁公平锁
新线程能否插队能(两次插队机会)不能
队列中等待线程的优先级无特殊保护等待最久的线程优先
吞吐量
可能产生饥饿
默认选择

4.2 公平锁的tryAcquire实现

// FairSync 中的 tryAcquire 方法 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 【关键区别】公平锁要求:队列中没有等待时间更长的线程 // hasQueuedPredecessors() 检查队列中是否有线程在等待 // 如果队列为空,或者当前线程是队列中等待最久的,才允许尝试获取 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 重入逻辑与非公平锁完全相同 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }

4.3 hasQueuedPredecessors:检查队列中是否有等待线程

public final boolean hasQueuedPredecessors() { Node t = tail; Node h = head; Node s; // 条件为真的情况:队列不为空,且头节点的后继节点不是当前线程 return h != t && // 队列中至少有2个节点(头节点 + 至少一个等待节点) ((s = h.next) == null || // 头节点的后继为空(极端情况) s.thread != Thread.currentThread()); // 后继节点的线程不是当前线程 }

这个方法的核心逻辑

  • 如果队列为空 → 返回false(没有等待者)

  • 如果队列只有哨兵节点 → 返回false(没有实际等待的线程)

  • 如果队列中第一个等待线程是当前线程 → 返回false(重入场景,允许获取)

  • 否则 → 返回true(有其他线程在队列中等待,当前线程不能插队)

4.4 公平锁加锁流程对比

非公平锁: 线程到来 → 尝试CAS抢锁 → 失败 → tryAcquire再尝试 → 失败 → 入队等待 公平锁: 线程到来 → tryAcquire检查队列 → 有等待者则直接入队 → 入队等待 (没有lock开头的快速CAS抢锁)

注意:公平锁的lock()方法中没有开头的CAS抢锁:

// FairSync.lock() 直接调用 acquire,没有快速尝试 final void lock() { acquire(1); }

4.5 非公平的两次插队机会(回顾)

非公平锁之所以"非公平",在于新来的线程有两次插队机会:

  1. 第一次插队lock()方法开头的compareAndSetState(0, 1)

  2. 第二次插队tryAcquire()中的compareAndSetState(0, acquires)

// NonfairSync.lock() final void lock() { if (compareAndSetState(0, 1)) // ← 插队机会1 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } // nonfairTryAcquire() if (c == 0) { if (compareAndSetState(0, acquires)) // ← 插队机会2 // ... }

五、完整加锁流程对比总结

5.1 非公平锁完整流程

线程调用 lock() │ ▼ 【插队1】CAS(state=0→1) 成功? ──是──▶ 设置owner → 返回 │ 否 ▼ tryAcquire() → nonfairTryAcquire() │ ├── state=0? ──是──▶ 【插队2】CAS抢锁 │ │ │ ├── 成功 → 设置owner → 返回 │ └── 失败 → 继续 │ ├── owner=当前线程? ──是──▶ state+1 → 返回成功 │ └── 其他情况 → 返回失败 │ ▼ addWaiter() → 创建节点加入队列 │ ▼ acquireQueued() → 自旋等待 │ ├── 前驱是头节点且tryAcquire成功? ──是──▶ 设为新头节点 → 返回 │ └── 否则 → shouldPark... → parkAndCheck... → 阻塞

5.2 公平锁完整流程

线程调用 lock() → acquire(1) │ ▼ tryAcquire() (FairSync版本) │ ├── state=0 且 队列中无等待者? ──是──▶ CAS抢锁成功 → 设置owner → 返回 │ ├── owner=当前线程? ──是──▶ state+1 → 返回成功 │ └── 其他情况 → 返回失败 │ ▼ addWaiter() → 创建节点加入队列 │ ▼ acquireQueued() → 自旋等待 (与非公平锁相同)

5.3 关键区别总结表

比较项非公平锁公平锁
lock()开头快速CAS✅ 有❌ 无
tryAcquire中state=0时直接CAS抢锁(不管队列)检查队列无等待者才CAS
插队次数2次0次
吞吐量
饥饿风险

六、常见问题深度解答

Q1:为什么AQS队列的头节点是空节点(哨兵)?

:这是一个设计优化,主要原因:

  1. 分离"当前持有锁的线程"和"队列管理"的职责

  2. 释放锁时,直接从头节点的后继节点开始唤醒,逻辑统一

  3. 避免空指针判断,简化代码

Q2:为什么unparkSuccessor要从尾部向前遍历?

:因为并发入队时,next指针可能不是最新的:

  • enq()方法中,先设置prev和CAStail,最后才设置next

  • 如果刚设置完tail但还没设置next,从头向后遍历会丢失新节点

  • prev指针一旦设置就不会改变,所以从尾部向前遍历是可靠的

Q3:非公平锁为什么性能更好?

:因为减少了线程挂起/唤醒的次数:

  • 公平锁:线程释放锁后,必须唤醒队列中的下一个线程

  • 非公平锁:新来的线程可能直接抢到锁,避免了唤醒开销

  • 在高并发场景下,减少上下文切换能显著提升吞吐量

Q4:如何实现超时获取锁?

:AQS提供了tryAcquireNanos方法,核心逻辑是:

java

private boolean doAcquireNanos(int arg, long nanosTimeout) { // ... for (;;) { // 尝试获取锁... nanosTimeout = deadline - System.nanoTime(); if (nanosTimeout <= 0L) return false; // 超时返回 if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanosTimeout); // 限时等待 // ... } }

Q5:AQS如何支持共享模式(如Semaphore)?

:共享模式与独占模式的区别:

  • 独占模式:state表示锁是否被占用(0/1或重入次数)

  • 共享模式:state表示可用资源数量(如Semaphore的许可数)

  • 共享模式的节点释放后,会传播唤醒后面的共享节点(PROPAGATE状态)


七、总结

7.1 核心知识点回顾

组件作用
state同步状态,ReentrantLock中表示重入次数
exclusiveOwnerThread当前持有锁的线程
head/tail同步队列的头尾指针
Node队列节点,封装等待线程和前驱后继关系
waitStatus节点状态,SIGNAL表示需要唤醒后继
LockSupport线程阻塞/唤醒的工具类

7.2 关键设计思想

  1. CAS + 自旋:无锁化实现线程安全的入队操作

  2. 模板方法模式:AQS定义骨架,子类实现钩子方法

  3. CLH队列变种:双向链表便于取消和唤醒

  4. 哨兵节点:简化边界条件处理

  5. 可重入设计:state计数 + owner线程判断

7.3 一句话总结

AQS通过一个volatile的state变量表示同步状态,通过一个FIFO的双向CLH队列管理等待线程,利用CAS+自旋实现无锁入队,通过LockSupport实现线程的阻塞与唤醒。非公平锁允许两次插队提升性能,公平锁通过hasQueuedPredecessors检查保证先来后到。

http://www.jsqmd.com/news/873855/

相关文章:

  • 2026亲测10款降AIGC网站红黑榜!优缺点无死角剖析,达标率对标顶级水准
  • µVision调试器与SEGGER J-Link兼容性解析
  • 【咨询业AI Agent应用成熟度评估模型】:基于217家机构实测数据的4级能力图谱与升级路线图
  • Docker 日常操作笔记(开发最常用命令)
  • 为什么iPhone微信聊天记录搜不到“?“,而安卓可以。
  • 混合精度优化在LLM推理加速中的实践与调优
  • Keil MDK中System Viewer空白问题的解决方案
  • 社交AI Agent不是Chatbot!5个被99%团队忽略的协议层设计陷阱(附LinkedIn/小红书级SDK接口规范)
  • 通过curl命令直接测试Taotoken聊天补全接口的配置与调用方法
  • AI赋能 绿色未来 —— 华硕重磅亮相第二十八届海峡两岸经贸交易会
  • 3个实用方法彻底解决阅读APP书源失效问题
  • Docker 里面的镜像(Image)和容器(Container)到底是什么
  • Python爬虫实战:爬取论文期刊 文献整理+管理表生成
  • Claude不是在模仿人,是在重构认知:3个被忽略的递归反思协议(附企业级调优checklist)
  • 5个技巧让你用Python零成本获取A股专业数据
  • Python、BMA-Stacking融合LightGBM、GBDT、KNN多模型电商交易欺诈风险预警研究|附代码数据
  • Apple ID身份协商协议全解析:rO/scnt/m动态参数生成原理
  • 三亚夜市哪家最有特色 - 资讯纵览
  • pycryptodome导入失败的四大底层原因与诊断方案
  • 非球面高精加高精密恒温恒湿空调机组选哪家 - 资讯纵览
  • 清远厂房搬家公司哪家专业靠谱?TOP5收费标准与避坑指南 - 从来都是英雄出少年
  • PostgreSQL 性能优化:从 3 秒到 30 毫秒,我做了这 5 件事
  • Meta裁了8000人,员工拖着行李箱抢可乐
  • 满帮季报图解:营收28亿,净利10亿 派息8750万美元
  • 碳化硅衬底与器件:怎么分辨有真产能的原厂和贸易商
  • eVTOL 结构件供应商,怎么从 480 万家工厂里找到真产能
  • 计算机组成原理 期末复习知识点总结
  • MoE稀疏激活原理与工程落地实战
  • Dell服务器数据恢复实战:RAID故障诊断与只读抢救指南
  • 无监督跌倒检测:基于IMU时序建模的异常识别工程实践