从 AQS 到 ReentrantLock:搞懂同步队列与条件队列,这一篇就够了

一、队列实现原理

1、同步等待队列和条件等待队列

![](https://oss.xyyzone.com/jishuzhan/article/2027651731342032897/b71dcdbfa87fa4b6bfc6cef001ffb397.webp) ## 二、同步等待队列 主要用于维护获取锁失败时入队的线程 ### 1、举个例子🌰🌰 **1.1、代码** ```csharp import java.util.concurrent.locks.ReentrantLock; /** * 模拟购买场景 */ public class ReentrantLockDemo { //默认非公平 private final ReentrantLock lock = new ReentrantLock(); // 总数 private static int count = 8; /** * 模拟购买 */ public void buy() { // 获取锁 lock.lock(); try { if (count > 0) { try { // 休眠1s Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":购买了第" + count-- + "张,同步队列长度"+ lock.getQueueLength()); } else { System.out.println(Thread.currentThread().getName() + ":购买失败,同步队列长度"+ lock.getQueueLength()); } } finally { // 释放锁 lock.unlock(); } } public static void main(String[] args) { ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo(); for (int i = 1; i <= 10; i++) { Thread thread = new Thread(() -> { reentrantLockDemo.buy(); // 抢票 }, "线程" + i); // 启动线程 thread.start(); } try { Thread.sleep(10000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("剩余:" + count); } } ``` **1.2、结果** ```makefile 线程1:购买了第8张,同步队列长度9 线程2:购买了第7张,同步队列长度8 线程3:购买了第6张,同步队列长度7 线程4:购买了第5张,同步队列长度6 线程5:购买了第4张,同步队列长度5 线程6:购买了第3张,同步队列长度4 线程7:购买了第2张,同步队列长度3 线程8:购买了第1张,同步队列长度2 线程9:购买失败,同步队列长度1 线程10:购买失败,同步队列长度0 剩余:0 ``` > ⚠️注意: > > ✔️负责管理在竞争锁失败时进入等待状态的线程,基于双向链表数据结构的队列,是FIFO先进先出线程等待队列,Java中的\[CLH队列\]是原CLH队列的一个变种,**线程由原自旋机制改为阻塞机制**。 ### 2、源码分析 **2.1、源码** ```ini public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } // 将当前线程封装为一个独占模式的节点(Node),并添加到AQS的同步队列尾部。 private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; } // 一个无限循环,用于让线程持续尝试获取资源 final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; //自旋等待逻辑 for (;;) { final Node p = node.predecessor(); //判断当前节点的前驱节点是否为头节点(head),并且尝试获取锁 if (p == head && tryAcquire(arg)) { //如果成功获取资源,则更新头节点、断开旧节点链接,并返回中断状态。 setHead(node); p.next = null; // help GC failed = false; return interrupted; } //挂起线程等待唤醒 //如果未能获取资源,则根据前驱节点状态决定是否挂起线 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } ``` > 📌 for (;;) 的核心意义: > > ✔️**自旋等待** :**头节点对应的线程** 会在队列中循环尝试获取锁,直到成功获取锁或被中断。这种方式***避免了频繁的线程阻塞与唤醒带来的内核态切换开销*** ,从而显著提升并发性能。即便线程被挂起后再次唤醒,也会***重新进入自旋逻辑继续竞争锁*** 。 > > ✔️**灵活控制** :通过循环条件动态判断是否可以获取资源或需要挂起线程。支持中断响应,增强程序的健壮性。 > > ✔️**高效资源管理**:成功获取资源后立即退出循环,减少不必要的计算。 *** ** * ** *** ## 三、条件等待队列 * 调用**await()** 的时候会***释放锁*** ,然后***线程会加入到条件队列*** * 调用**signal()** 唤醒的时候会***把条件队列中的线程节点移动到同步队列中,等待再次获得锁*** ### 1、举个例子🌰🌰 **1.1、代码** ```csharp import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class ConditionDemo { /** * 锁 */ private static ReentrantLock lock = new ReentrantLock(); /** * 判断条件 */ private static Condition condition = lock.newCondition(); /** * 条件标志位 */ private static volatile boolean flag = false; public static void main(String[] args) throws InterruptedException { new Thread(new DemoOne(), "DemoOne").start(); Thread.sleep(1000); new Thread(new DemoTwo(), "DemoTwo").start(); } /** * DemoOne */ static class DemoOne implements Runnable { @Override public void run() { lock.lock(); try { while (!flag) { System.out.println(Thread.currentThread().getName() + "当前条件不满足等待"); try { // TODO:something condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + "接收到通知条件满足"); } finally { lock.unlock(); } } } /** * DemoTwo */ static class DemoTwo implements Runnable { @Override public void run() { lock.lock(); try { flag = true; System.out.println(Thread.currentThread().getName() + "唤醒DemoOne"); condition.signal(); } finally { lock.unlock(); } } } } ``` **1.2、结果** DemoOne当前条件不满足等待 DemoTwo唤醒DemoOne DemoOne接收到通知条件满足 > ⚠️注意: > > ✔️条件等待队列会把条件等待队列中的线程节点移动到同步队列中,等待再次获得锁 > > ✔️如果同步等待队列为空时,会唤醒条件等待队列的第一个节点(node) ### 2、源码分析 **2.1,源码** ```java // 如果存在等待时间最长的线程,则将其从该条件的等待队列移至拥有该锁的等待队列 public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignal(first); } // 移除并转移节点,直到遇到未被取消的节点或空值 private void doSignal(Node first) { do { //将 first.nextWaiter 赋值给 firstWaiter,即让 firstWaiter 指向当前节点的下一个等待者 //如果 first.nextWaiter 为 null,说明当前节点是队列中最后一个节点,因此将 lastWaiter 设置为 null,表示队列为空 if ( (firstWaiter = first.nextWaiter) == null) //断开当前节点的链接 lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) && (first = firstWaiter) != null); // 尝试唤醒当前节点 // 如果 firstWaiter 为 null,说明队列已遍历完毕,退出循环 } // 将节点从条件队列转移到同步队列 final boolean transferForSignal(Node node) { //使用 CAS 操作将节点的 waitStatus 从 Node.CONDITION 改为 0。 //如果失败,说明该节点已经被取消(cancelled),直接返回 false。 if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; //调用 enq(node) 将节点加入同步队列(Sync Queue)。 //返回值 p 是该节点在同步队列中的前驱节点。 Node p = enq(node); int ws = p.waitStatus; //获取前驱节点 p 的 waitStatus。 //如果前驱节点的状态大于 0(表示已取消),或者无法将其状态设置为 Node.SIGNAL,则直接唤醒当前节点的线程。 //否则,依赖前驱节点来唤醒当前节点。 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; } ``` > ⚠️注意 > > ✔️基于*LockSupport.unpark(node.thread)、LockSupport.park(node.thread)* 实现阻塞和唤醒 > > ✔️**条件队列在AQS内部是一个单向链表,节点在被转移到同步队列时,被构建为双向链表节点CLH队列** *** ** * ** *** ## 四、总结 ### **1、ReentrantLock 基于 AQS+CAS 实现了同步队列与条件队列两大核心结构:** * **同步队列:** 负责管理抢锁失败的线程,通过***头节点自旋***减少线程切换开销,提升并发性能; * **条件队列** :用于实现精准的等待 / 通知机制,一个锁可以支持多个条件队列,解决了传统通知***无法精准唤醒的问题***。 \[![](https://oss.xyyzone.com/jishuzhan/article/2027651731342032897/72d23482a0003f810732e6cb6d58664c.webp) ### **2、ReentrantLock 与** synchronized**相比:** * synchronized:只有一套隐式的等待队列,使用简单但功能单一; * ReentrantLock :依靠**同步队列 + 条件队列** 的配合,在锁的灵活性、可控性、并发调度精度上都更加强大,是***复杂并发场景下更优的选择***。

相关推荐
鱼人2 小时前
Nginx 全能指南:从反向代理到负载均衡,一篇打通任督二脉
后端
UIUV2 小时前
node:child_process spawn 模块学习笔记
javascript·后端·node.js
Java编程爱好者2 小时前
如果明天 Spring 框架突然从世界上消失,Java 会发生什么?
后端
神奇小汤圆3 小时前
Spring让Java慢了30倍,JIT、AOT等让Java比Python快13倍,比C慢17%
后端
颜酱3 小时前
单调栈:从模板到实战
javascript·后端·算法
神奇小汤圆3 小时前
支付成功订单却没了?MyBatis连接池的坑我踩了
后端
雨中飘荡的记忆5 小时前
OpenClaw:开源AI助手平台的革命之路
后端
程序员鱼皮6 小时前
GitHub 关注突破 2w,我总结了 10 个涨星涨粉技巧!
前端·后端·github
用户298698530146 小时前
程序员效率工具:Spire.Doc如何助你一键搞定Word表格排版
后端·c#·.net