尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深入理解AQS:同步队列、条件队列与waitStatus全解析

深入理解AQS:同步队列、条件队列与waitStatus全解析 前言AQSAbstractQueuedSynchronizer是 Java 并发包的基石但很多同学在学习时容易混淆几个核心概念同步队列和条件等待队列到底有几条共享锁和独占锁是不是各有各的队列waitStatus的五种状态究竟如何流转本文将彻底澄清这些误区带你把 AQS 的队列模型和状态机一次吃透。一、同一个 AQS只有一条同步队列先纠正一个流传甚广的误解❌错误理解共享锁有一套自己的同步队列独占锁也有自己的同步队列互相隔离。✅真相同一个 AQS 实例只维护一条同步队列CLH 变种双向链表。Condition 条件队列可以有多条和共享/独占模式无关。以最典型的读写锁ReentrantReadWriteLock为例ReentrantReadWriteLock rwLock new ReentrantReadWriteLock();ReadLock readLock rwLock.readLock(); //共享锁WriteLock writeLock rwLock.writeLock(); //独占锁读锁和写锁底层共用同一个 AQS 实例所有等待线程——无论是读线程还是写线程——都排在同一条同步队列里。队列中的节点通过nextWaiter字段区分模式nextWaiter SHARED一个静态常量节点→ 共享节点读线程nextWaiter null→ 独占节点写线程一条典型的混合队列可能长这样Head ← 读线程(共享) ← 读线程(共享) ← 写线程(独占) ← 读线程(共享)重要规则一旦队列中出现独占写节点后续所有新来的读线程都会被阻塞必须排队。这种设计正是为了解决读锁持续重入导致写锁饥饿的问题。二、分清两条队列同步队列 vs. 条件队列AQS 里确实存在“两条队列”但它们是不同维度的概念同步队列Sync Queue- 一个 AQS 只有唯一一条。- 双向链表依赖Node.prev和Node.next。- 存放所有等待抢占锁的线程节点共享/独占节点混排其中。条件等待队列Condition Queue- 可以有多条每次调用lock.newCondition()就创建一条。- 单向链表依赖Node.nextWaiter串联。- 存放调用await()后释放锁、等待特定条件的线程。-与共享/独占模式无关只看线程是否调用了await()。一个关键限制只有独占模式才能创建 Condition。ReentrantReadWriteLock的读锁共享锁不支持newCondition()因为多个线程同时持有读锁时await语义无法实现。三、Node 如何在两条队列间流转同一个Node对象可以先后存在于同步队列和条件队列但同一时刻只能属于其中一条。转移时两套链表指针会彻底“换乘”。Node内部有三个关键引用volatile Node prev; // 同步队列前驱双向链表volatile Node next; // 同步队列后继双向链表Node nextWaiter; // 条件队列后继单向链表 / 节点模式标记完整流转过程await → signal线程持有锁时Node位于同步队列头部持有锁的节点只是逻辑上的头实际可能已出队。调用condition.await()- 将waitStatus设为CONDITION(-2)。-从同步队列脱离prev、next置空。- 通过nextWaiter加入条件队列的尾部。其他线程调用signal()- 将条件队列头部节点移出。-waitStatus从-2重置为0。-清空nextWaiter重新使用prev/next挂入同步队列尾部重新参与锁竞争。通俗比喻Node 是一个人同步队列是办事大厅的排队区双向队列条件队列是旁边的休息室单向队列。从休息室被叫号后必须回到排队区重新排队。四、nextWaiter的双重身份nextWaiter字段被精心复用在不同场景下有不同含义在条件队列中作为单向链表的后继指针指向下一个等待同一条件的节点。在构造节点入同步队列时标记当前线程是共享模式还是独占模式。-nextWaiter SHARED一个空常量节点 → 共享节点-nextWaiter null→ 独占节点这种复用是 Doug Lea 的经典内存优化用同一个字段承载两类完全不相干的信息因为一个节点不会同时存在于条件队列和初始模式标记的场景中。五、waitStatus五种状态完全解析waitStatus是volatile int类型存放在每个Node中是 AQS 状态机运转的核心。它只有五种取值常量值所在队列核心含义00同步队列默认初始状态无特殊标记SIGNAL-1同步队列前驱节点释放锁后必须唤醒当前后继CANCELLED1同步队列线程放弃竞争节点作废等待被清理CONDITION-2Condition 条件队列节点位于条件队列中不在同步队列PROPAGATE-3同步队列仅共享锁共享锁唤醒需要无条件向后传播关键分界线- 同步队列只包含0、-1、1、-3。- 条件队列只包含-2。--2的节点永远不会在同步队列中出现一旦signal()转移回同步队列waitStatus会被重置为0。5.1 0初始状态新节点加入同步队列时的默认值。当一个节点成功获取锁并正在执行时通常保持 0。5.2 SIGNAL-1—— 最核心的状态“前驱节点你释放锁时记得叫醒我”。当前线程在阻塞前会将自己的前驱节点的waitStatus通过 CAS 设为-1。前驱节点执行完release()后发现自己是-1就知道有后继在等待必须执行unparkSuccessor()。独占锁和共享锁都会大量使用-1它不是独占模式的专利。5.3 CANCELLED1—— 作废节点永不唤醒当线程在同步队列中等待超时或被中断时会将自己的waitStatus设置为1放弃竞争。该状态是单向的一旦变为 1永不可逆。重要被标记为 CANCELLED 的节点永远不会被唤醒。前驱节点释放锁时会跳过所有ws 0的节点同时顺便修改指针将作废节点从链表中剔除惰性清理主要发生在入队、释放、被唤醒后的清理步骤中。5.4 CONDITION-2—— 条件队列专属节点调用await()后进入条件队列时被标记。此时prev和next已清空节点完全脱离同步队列。只有signal()能将其转移回同步队列并重置状态为 0。5.5 PROPAGATE-3—— 共享锁的传播守护这个状态仅在共享锁释放的瞬间、由头节点临时设置普通等待节点绝不会被标记为-3。场景共享锁允许多个线程同时持有当锁释放并唤醒后继共享线程后必须保证唤醒动作“向后传播”否则在高并发下会出现后继共享线程永久休眠的 bug。PROPAGATE的作用就是标记头节点“本次释放必须无条件向后传播唤醒信号”确保一连串的读线程都能被顺利唤醒。特别注意在共享锁的同步队列里绝大多数节点依然是SIGNAL(-1)-3只是头节点的临时流转状态。六、共享锁队列的混排与误区澄清结合waitStatus和节点类型可以描绘出共享锁队列的真实面貌Head(ws-3) ← N1(ws-1, shared) ← N2(ws-1, exclusive) ← N3(ws-1, shared)Head 临时为-3以保障传播N1、N2、N3 都是普通等待节点各自的前驱被标记为-1N1 和 N3 是读线程nextWaiterSHAREDN2 是写线程nextWaiternull。三个核心误区务必纠正❌ 共享锁节点固定是-3→ ✅-3只临时出现在头节点普通排队节点仍是-1。❌ 共享锁和独占锁各有独立同步队列 → ✅ 共用一条队列共享/独占只是节点标记。❌ CANCELLED 节点被唤醒后自行退出 → ✅ 该节点永不唤醒由其他线程在清理时直接跳过并移除。七、高频面试总结队列数量一个 AQS 一条同步队列双向newCondition()可创建多条条件队列单向。节点类型共享/独占通过nextWaiter标记混排在同步队列中而非各自独立。Condition 限制只有独占模式支持 Condition读锁不可创建。waitStatus 核心--1SIGNAL通用唤醒标记--2CONDITION条件队列专属--3PROPAGATE共享锁头节点临时使用-1CANCELLED作废单向不可逆-0默认初始状态。同一时刻一个 Node 只属于一条队列在队列间迁移会清空旧指针。拓展思考为什么同步队列采用双向链表而条件队列只用单向链表同步队列需要频繁的删除作废节点、从尾部向前查找有效前驱等操作必须有prev指针支持回溯条件队列只需从头部取出节点执行signal单向遍历即可满足需求无需前驱引用节省空间。通过以上梳理相信你已经可以清晰画出 AQS 的整体队列结构并对waitStatus的流转如数家珍。在面试和实际开发中这些底层机制将帮助你更好地理解 Java 锁的实现与并发问题排查。
返回列表