AQS

AQS概念

 一、AQS 是什么?它的核心作用是什么?(必考基础)

  • 答案:
    • AQS 是 java.util.concurrent.locks 包下的一个抽象类
    • 它是构建(如 ReentrantLock)和同步器(如 SemaphoreCountDownLatchReentrantReadWriteLock)的核心框架
    • 核心作用: 提供了一个基于 FIFO 等待队列的模板,用于实现阻塞锁依赖特定状态(state)的同步器。它封装了线程排队、阻塞、唤醒的复杂逻辑,子类只需关注状态(state)的管理和获取/释放条件

 二、AQS 的核心思想与关键组件(重中之重)

  1. 同步状态 state (int 类型):
    • 是什么? AQS 管理的核心状态变量,子类决定其含义。例如:
      • ReentrantLock: 表示锁的重入次数(0 表示未锁定)。
      • Semaphore: 表示剩余的许可证数量。
      • CountDownLatch: 表示还需要倒数的计数。
    • 如何访问? 通过 getState()setState(int newState)compareAndSetState(int expect, int update) (CAS 操作) 方法进行原子操作。
    • 为什么是 int 足够表达大多数同步场景的状态(重入次数、资源数等),且 CAS 操作高效。子类也可以使用位运算管理多个状态(如 ReentrantReadWriteLock)。
  2. FIFO 等待队列 (CLH 队列的变种):
    • 是什么? 一个双向链表,用于存放等待获取资源的线程。
    • 节点 Node: 队列中的每个节点代表一个等待线程。关键属性:
      • waitStatus: 节点状态(CANCELLEDSIGNALCONDITIONPROPAGATE)。SIGNAL 是最重要的,表示该节点的后继节点需要被唤醒。
      • prevnext: 指向前驱和后继节点的指针。
      • thread: 等待线程的引用(可能为 null)。
      • nextWaiter: 指向下一个在条件队列ConditionObject)上等待的节点,或用于区分独占模式共享模式
    • head 和 tail: 队列的头尾指针(头节点是虚节点,不代表任何线程)。
  3. 模板方法模式 (Template Method Pattern):
    • AQS 的核心设计模式。 AQS 定义了获取/释放资源的主流程(如 acquire(int arg)release(int arg)),但将关键决策留给子类实现。
    • 子类需要重写的核心 protected 方法:
      • tryAcquire(int arg)独占模式下尝试获取资源。成功返回 true,失败返回 false。
      • tryRelease(int arg)独占模式下尝试释放资源。成功返回 true,失败返回 false。
      • tryAcquireShared(int arg)共享模式下尝试获取资源。返回值 >=0 表示成功(值代表剩余资源数),<0 表示失败。
      • tryReleaseShared(int arg)共享模式下尝试释放资源。如果释放后可能唤醒后续等待者则返回 true。
      • isHeldExclusively(): 当前线程是否独占持有资源(主要用于 Condition)。

 三、AQS 的核心流程(关键流程要清晰)

面试官会让你描述主要流程,特别是 acquire 和 release

  1. 独占模式获取资源 (acquire(int arg)):java复制下载public final void acquire(int arg) { if (!tryAcquire(arg) && // 1. 子类尝试直接获取 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 2. 获取失败,将线程包装成独占节点加入队列尾部 selfInterrupt(); // 3. 如果在等待过程中被中断过,补上中断标记 }
    • 步骤详解:
      1. 调用子类实现的 tryAcquire(arg) 尝试直接获取资源(非阻塞)。
      2. 如果失败:
        • addWaiter(Node.EXCLUSIVE):将当前线程包装成一个独占模式(Node.EXCLUSIVE 的节点,快速 CAS 尝试入队尾。如果失败(竞争),则调用 enq(node) 方法自旋 CAS 直到成功入队尾
        • acquireQueued(final Node node, int arg):节点入队后,在此方法中自旋
          • 检查前驱节点是否是 head(说明马上轮到自己了),如果是则再次调用 tryAcquire(arg) 尝试获取。
          • 如果获取成功,将自己设为新的 head(原 head 出队),返回。
          • 如果获取失败 或 前驱不是 head
            • 调用 shouldParkAfterFailedAcquire(p, node):检查并设置前驱节点的 waitStatus 为 SIGNAL(表示“我挂了请唤醒我”),并返回是否需要阻塞。
            • 如果需要阻塞,调用 parkAndCheckInterrupt():使用 LockSupport.park() 阻塞当前线程。线程被唤醒后,检查是否被中断过。
      3. 如果在阻塞等待过程中被中断过(Thread.interrupted() 返回 true),则在返回上层后调用 selfInterrupt() 补上中断状态(AQS 在等待过程中不响应中断,但会记录中断状态,获取到资源后才响应)。
  2. 独占模式释放资源 (release(int arg)):java复制下载public final boolean release(int arg) { if (tryRelease(arg)) { // 1. 子类尝试释放资源 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); // 2. 唤醒头节点的有效后继节点 return true; } return false; }
    • 步骤详解:
      1. 调用子类实现的 tryRelease(arg) 尝试释放资源。
      2. 如果释放成功(资源完全释放,state==0):
        • 检查头节点 h 是否存在且 waitStatus != 0(通常为 SIGNAL,表示它有后继需要唤醒)。
        • 调用 unparkSuccessor(h):找到头节点 h 后面第一个 waitStatus <= 0(非取消状态)的节点,使用 LockSupport.unpark(s.thread) 唤醒该节点对应的线程
  3. 共享模式获取/释放 (acquireShared/releaseShared):
    • 流程与独占模式类似,主要区别:
      • 节点类型是 Node.SHARED
      • tryAcquireShared 返回值 >=0 表示成功(值代表剩余资源数)。
      • 在 doAcquireShared 中,成功获取后(通常是前驱是 head 且 tryAcquireShared >=0),不仅将自己设为新 head,还会调用 setHeadAndPropagate(node, r) 尝试唤醒后续的共享节点(传播性,如 Semaphore 释放多个许可证时)。
      • releaseShared 释放后必须调用 doReleaseShared() 确保唤醒后继(即使 tryReleaseShared 返回 true 也可能需要唤醒,因为共享模式允许多个线程同时获取)。

 四、关键机制与特性(深入理解点)

  1. 为什么是双向链表 (CLH 变种)?
    • 主要为了高效处理取消操作。当一个节点(线程)取消等待(超时或中断)时,需要将自己从队列中移除。双向链表可以方便地修改 prev 和 next 指针,确保移除操作 O(1) 完成。单向链表移除中间节点需要遍历。
  2. waitStatus 的作用:
    • CANCELLED (1): 节点因超时或中断取消。需要被移除。
    • SIGNAL (-1)最重要! 表示该节点的后继节点需要被唤醒。节点在阻塞前,必须确保其前驱的 waitStatus 是 SIGNAL
    • CONDITION (-2): 节点在条件队列ConditionObject)中等待。
    • PROPAGATE (-3)仅用于共享模式头节点。表示下一次 acquireShared 应该无条件传播(唤醒后续共享节点)。解决共享释放的竞争问题。
    • 0: 初始状态。
  3. 公平锁 vs 非公平锁 (以 ReentrantLock 为例):
    • 公平锁: 严格按照 FIFO 队列顺序获取锁。tryAcquire 实现:如果 state==0 且队列中没有其他等待线程(!hasQueuedPredecessors(),才尝试 CAS 获取。
    • 非公平锁: 新来的线程可以“插队”,直接尝试 CAS 获取 state,失败后再入队。tryAcquire 实现:直接尝试 CAS 获取 state,不管队列情况。
    • 优缺点: 公平锁避免饥饿,但吞吐量通常低于非公平锁(唤醒线程开销大)。非公平锁吞吐量高,但可能导致线程饥饿。
  4. Condition 条件队列 (ConditionObject):
    • AQS 内部类,实现 Condition 接口。
    • 核心: 每个 ConditionObject 维护一个单向的条件等待队列
    • await(): 释放锁 -> 创建 CONDITION 节点加入条件队列 -> 阻塞。
    • signal(): 将条件队列头节点(等待最久的)转移到 AQS 主队列尾部,等待被唤醒竞争锁。
    • signalAll(): 转移条件队列所有节点到主队列。
    • 关键点: 一个 AQS 实例(锁)可以有多个 ConditionObject(多个等待条件)。

 五、典型应用(知道如何基于 AQS 实现)

面试官可能让你简述某个同步器如何利用 AQS:

  1. ReentrantLock (独占):
    • state 表示锁的重入计数(0 未锁定)。
    • tryAcquire: 如果 state==0 则 CAS 尝试获取(非公平)或 检查队列后再获取(公平);如果 state>0 且当前线程是持有者,则 state++(重入)。
    • tryRelease: state–;只有当 state==0 时才真正释放(返回 true)。
  2. Semaphore (共享):
    • state 表示可用许可证数量。
    • tryAcquireShared: 计算 available = state - acquires。如果 available < 0 或 CAS 设置 state = available 成功,则返回 available(<0 失败,>=0 成功)。
    • tryReleaseShared: 自旋 CAS 增加 state (state += releases),返回 true(总是可能唤醒等待者)。
  3. CountDownLatch (共享):
    • state 表示需要倒数的计数。
    • tryAcquireShared: 只要 state == 0 就返回 1(成功),否则返回 -1(失败)。等待线程调用 await() -> acquireSharedInterruptibly(1)
    • tryReleaseShared: 每次 countDown() 调用 CAS 减少 state。当 state 被减到 0 时,返回 true(唤醒所有等待线程)。
  4. ReentrantReadWriteLock (组合):
    • 使用一个 AQS 实例。巧妙利用 state (int) 的高 16 位表示读锁(共享)计数,低 16 位表示写锁(独占)计数。
    • 实现更复杂,需要处理读写互斥、读读共享、写写互斥、写锁降级等。

 六、如何准备

  1. 重点掌握: 核心思想(模板方法、state、CLH队列)、独占模式 acquire/release 流程、waitStatus(特别是 SIGNAL)、公平/非公平区别。
  2. 理解典型应用: 至少能清晰说出 ReentrantLock 和 Semaphore/CountDownLatch 是如何基于 AQS 实现的。
  3. 动手实践: 尝试阅读 ReentrantLock 的源码(特别是 NonfairSync/FairSync 和 Sync 类),理解它们与 AQS 的协作。
  4. 清晰表述: 能用流程图或简洁语言描述 acquire 和 release 的主要步骤。
  5. 区分概念: AQS 本身不是锁,它是构建锁的框架;Lock 接口定义了锁的行为,ReentrantLock 等是具体实现,它们依赖 AQS。
  6. 了解即可(非重点): 条件队列(ConditionObject)的详细流程、PROPAGATE 状态解决的极端情况、复杂的取消逻辑。秋招深度通常不会挖到这么细。

ReentrantReadWriteLock

是解决“读多写少”场景的核心方案。

常与synchronizedReentrantLock对比,例如:

  • “为什么读写锁比互斥锁更适合缓存场景?”
  • “读写锁和StampedLock的区别是什么?

高频考点梳理

1. 核心特性(100%出现)

  • 规则:读读共享、读写互斥、写写互斥。
  • 适用场景:读频率 >> 写频率(如缓存、数据库查询)。
  • 性能优势:读操作并发执行,减少线程阻塞,提升吞吐量(对比synchronized)。

2. ReentrantReadWriteLock 实现细节(80%出现)

  • 公平性:默认非公平锁(吞吐量高),可设置为公平锁(防止饥饿但性能低)。
  • 可重入性:同一线程可重复获取读锁或写锁。
  • 锁降级写锁 → 读锁(允许),读锁 → 写锁(禁止)
    典型用例:先持写锁修改数据,再持读锁保证后续读取一致性。

3. 缺陷与解决方案(60%出现)

  • 锁饥饿问题:持续读锁导致写线程无法获取锁(可通过公平锁缓解)。
  • 锁升级不支持:持有读锁时无法直接升级为写锁(需先释放读锁)。
  • 替代方案StampedLock的乐观读(无锁快照)解决读写竞争问题。

4. 场景应用题(40%出现)

  • 缓存系统设计
    如何用ReentrantReadWriteLock实现线程安全的缓存(参考CacheDemo代码)。
  • 数据库连接池
    读连接可共享,写连接需独占。

 如何准备

  1. 理解核心思想:明确读写分离的适用场景(读 >> 写),并能解释其性能优势。
  2. 掌握锁降级流程:写锁 → 读锁的代码实现(需在释放写锁前获取读锁)。
  3. 对比其他锁
    • StampedLock对比:后者通过乐观读避免写线程饥饿。
    • synchronized对比:读写锁在低竞争时性能无优势,高并发读时显著优化。
  4. 代码实操:手写缓存类(如MyCache),演示读写锁的正确使用。

散々雨に降られたって 笑っていられる 即使被雨水淋湿也能笑着面对。----孤独摇滚
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇