AQS 与显式同步器

共 63 题
📑 题目列表 63 题
#
★★★

1. AQS 共享模式中的传播机制如何支持 Semaphore 和 CountDownLatch,何时会连续唤醒后继节点

AQS 的共享模式(shared mode)中,当 tryAcquireShared 返回非负值时,releaseShared 会触发对后继节点的传播唤醒(propagation),这种连续的唤醒机制如何让 Semaphore 和 CountDownLatch 得以工作?

  • AQS 共享模式与独占模式(setHead 与 setHeadAndPropagate)的区别
  • doReleaseShared 的传播链与短路条件
  • Semaphore 靠可重复获取许可、CountDownLatch 靠一次性归零的实现差异

共享模式下,允许多个线程同时获取"许可",这正是 Semaphore 与 CountDownLatch 的基础。当 releaseShared 成功释放后,AQS 调用 doReleaseShared,将 head 的状态从 SIGNAL 改为 0 并 unpark 后继节点;被唤醒的线程在 acquireShared 中成功拿到共享许可后,若还有剩余资格(tryAcquireShared 返回值仍是正数),会通过 setHeadAndPropagate 继续唤醒后继,形成"传播链"。对 Semaphore 而言,每次 release 可能让多个线程依次获得许可,因此需要连续唤醒;对 CountDownLatch 而言,count 归零后所有等待线程共享"已经打开"的信号,setHeadAndPropagate 会唤醒所有排队节点。传播链的短路条件是发现 head 已经处理过(head 状态为 0 或 head 未变化),避免无限次唤醒。

关键在 setHeadAndPropagate 里的 propagate 判断:仅当共享许可仍有剩余(tryAcquireShared 返回值 > 0)或 head 需要唤醒时才继续,否则停止,这正是"何时连续唤醒后继节点"的答案。相比独占模式,共享模式把"释放一个唤醒多个"的语义显式转成"唤醒一个、成功者再唤醒下一个"的接力,从而在极高唤醒下也能控制唤醒次数。

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                unparkSuccessor(h);
            } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        if (h == head) break;
    }
}
#
★★★

2. AQS 在 StampedLock 中的角色

StampedLock 并不直接继承 AQS,那么 AQS 在 StampedLock 的实现中扮演了怎样的角色?

  • StampedLock 的读写锁队列复用 AQS 的内部构架
  • StampedLock 内部使用 AQS 的 Node 结构与锁状态设计
  • 乐观读不经过 AQS 队列

StampedLock 内部借鉴了 CLH 队列锁思想,自建 WNode 节点结构(包含 prev、next、cowait、thread、status 等字段)来组织阻塞的写锁和悲观读锁线程,但它的核心状态机(state 中高 8 位版本号、低 7 位读写锁计数)是自定义的,不依赖 AQS 的 state 为 0/1 的语义。写锁、悲观读锁的获取与释放通过 CAS 修改 state 并借助自旋/队列挂起实现,而乐观读(tryOptimisticRead)完全不进入队列,只读取版本号。因此准确地说,StampedLock 借鉴了 CLH/AQS 的队列思想与类似节点数据结构,但并未继承 AQS 的 acquire/release 模板方法,也未直接复用 AQS 的 Node 内部类(它使用自己定义的 WNode)。

常见误区是把 StampedLock 当作 AQS 子类。实际上它通过自建 WNode 队列(结构类似 AQS 的 Node,但字段与语义不同)实现排队,并自己实现了 acquireRead/acquireWrite 等逻辑。这种设计让 StampedLock 能提供乐观读这种 AQS 完全没有的能力,代价是实现复杂度高、不可重入、语义更脆弱。

#
★★★

3. AQS 的 CLH 队列锁变种实现

AQS 使用 CLH 队列锁的变体来实现等待队列,请说明其节点结构、前驱等待与自旋/挂起策略?

  • CLH 锁的原始思想与 AQS 变体的差异
  • Node 的 prev/next 与 waitStatus 状态位
  • 入队先自旋后挂起(park)的策略

原始 CLH 锁基于隐式前驱链表,每个节点自旋等待前驱节点释放;AQS 将其改写为显式 prev/next 双向链表,并引入 waitStatus 字段(CANCELLED=1、SIGNAL=-1、CONDITION=-2、PROPAGATE=-3)。入队时通过 enq 用 CAS 把节点追加到 tail;获取失败时,先检查前驱是否为 head(是则说明马上轮到,可尝试再次获取),否则根据前驱的 waitStatus 决定是否 park 挂起。AQS 的变体用 waitStatus=SIGNAL 表示"前驱释放后需要唤醒我",从而支持阻塞式等待而非纯自旋,兼顾了低竞争下的低延迟与高竞争下的 CPU 节省。

AQS 选择 CLH 变体的原因是:锁释放时只需唤醒严格的 FIFO 后继节点,天然公平、无饥饿;prev/next 双向链便于在取消时前后链接;waitStatus 让线程能阻塞而不是持续空转。相比原始的 CLH,AQS 变体把"自旋等待前驱"换成"设置 SIGNAL 后 park",避免长时间占用 CPU。

#
★★★

4. AQS 的 acquire 失败后如何入队、挂起并被唤醒,tryAcquire 和 tryRelease 的重写契约分别是什么

AQS 的 acquire 在 tryAcquire 失败后如何将当前线程入队、挂起并最终被唤醒?请说明 tryAcquire 与 tryRelease 的重写契约?

  • acquire 模板方法的完整流程(tryAcquire → addWaiter → acquireQueued)
  • 入队、循环自旋、park 与 unpark 的交互
  • tryAcquire/tryRelease 的返回语义契约

acquire 的流程是:先调用 tryAcquire 尝试获取;失败则 addWaiter 把当前线程封装成节点入队;随后 acquireQueued 在循环中检查前驱是否为 head(若是则再次 tryAcquire,成功就 setHead 出队),否则根据前驱状态 park 挂起,直到被前驱的 unparkSuccessor 唤醒。tryAcquire 的契约是:返回 true 表示获取成功并已设置 state,返回 false 表示失败且不修改任何状态;tryRelease 的契约是:返回 true 表示释放成功且 state 已减到可以唤醒后继,返回 false 表示释放失败。两者都必须在 CAS 语义下保证原子地更新 state。

关键点在于 tryAcquire 失败时不得留下副作用,否则后续重试会出错;tryRelease 必须返回是否真能唤醒后继(state 归零),让 AQS 决定是否 unpark。重写者只需实现"如何改变 state",而"何时入队、何时 park、何时唤醒"全部由 AQS 模板方法自动完成。

#
★★★

5. AQS 的 acquire/release 流程与 tryAcquire 重写

请描述 AQS 独占模式的 acquire 与 release 完整流程,并说明 tryAcquire 的作用?

  • acquire 与 release 的模板方法调用链
  • tryAcquire 抽象方法的意义
  • 公平与非公平在 tryAcquire 中的差异

独占模式 acquire 流程:先 tryAcquire 尝试获取;失败则 addWaiter 入队,进入 acquireQueued 循环,前驱为 head 时再试 tryAcquire,成功则 setHead 出队并返回,否则 park 挂起。release 流程:先 tryRelease 修改 state,若返回 true 则 unparkSuccessor 唤醒后继节点。tryAcquire 是模板方法留给子类实现的抽象方法,子类通过它决定获取锁的语义(如 ReentrantLock 的公平锁在 tryAcquire 中先检查 hasQueuedPredecessors,非公平锁则直接 CAS 抢占)。

这种"模板方法 + 钩子"设计让 AQS 复用复杂的排队、阻塞、唤醒机制,而把"锁的语义"下沉到子类。公平性的差异就体现在 tryAcquire 是否检查队列中已有前驱等待者。

#
★★★

6. AQS 的 addWaiter 失败回退到 enq 的意义

AQS 的 addWaiter 方法先尝试快速入队,失败时才回退到 enq 循环,这样设计的意义是什么?

  • addWaiter 的快速路径(tail 非空时直接 CAS 追加)
  • enq 的循环处理(tail 为空初始化的特殊情况)
  • 并发下 CAS 失败的兜底

addWaiter 先检查 tail 是否为空,非空时用 CAS 把新节点挂在 tail 之后,若 CAS 成功则完成入队,这是无竞争时的快速路径。当 tail 为空(队列尚未初始化)或 CAS 失败(并发下其他线程抢先修改了 tail)时,回退到 enq 方法,enq 在死循环中处理两种情况:tail 为空则 CAS 初始化一个哨兵 head,否则 CAS 追加节点,直到成功。这样既保证低竞争下的高性能,又保证高竞争或初始化场景下的正确性。

快速路径避免每次入队都走重循环,而 enq 的兜底保证了队列初始化与并发追加的正确性。这是典型的"乐观快路径 + 悲观重试"模式,是 AQS 高性能的关键手法之一。

#
★★★

7. AQS 的 propagate 与共享模式的传播

AQS 的 Node.PROPAGATE 状态究竟解决了什么问题?在共享模式下它是如何保证传播的?

  • PROPAGATE 状态的引入动机(修复 JDK 6 的并发唤醒丢失 bug)
  • setHeadAndPropagate 与 doReleaseShared 的协作
  • 与 Semaphore 等共享同步器的联系

Node.PROPAGATE(-3)状态是 JDK 修复 6781020 号 bug 时引入的。问题是:共享模式下,若释放与获取并发进行,release 可能只唤醒一个后继,而该后继在获取前 head 已被其他线程替换,导致 setHeadAndPropagate 判断"propagate 不需要"而停止,其后的线程永远得不到唤醒。通过把 head 的状态置为 PROPAGATE,doReleaseShared 在 CAS 时发现 head 状态为 0 且 head 未变,会尝试把状态改为 PROPAGATE 并继续传播,从而保证共享许可的唤醒不会被"丢失"。

PROPAGATE 本质上是"提醒后来的线程:即使 head 是 0,也应继续传播唤醒"的标记,解决了共享模式下信号丢失的竞态。它是理解共享同步器(Semaphore、CountDownLatch)正确性的关键。

#
★★★

8. AQS 等待节点取消后如何从同步队列中跳过或清理,取消风暴会造成哪些遍历成本

当获取锁的线程被中断或超时导致节点取消后,AQS 如何从队列中清理这些节点?"取消风暴"会带来哪些遍历成本?

  • waitStatus 的 CANCELLED 状态与取消语义
  • cancelAcquire 中向前遍历清理的过程
  • 取消风暴下 O(n) 遍历与竞争加剧

取消时,节点 waitStatus 置为 CANCELLED。清理主要通过两种方式:一是 acquireQueued 循环中,若前驱是 CANCELLED 节点,则向前遍历(pred 链)跳过并摘除它;二是 cancelAcquire 成功后,用 prev 指针向前遍历,把连续的 CANCELLED 节点从链表中清掉,并修正前驱的 next 指针。取消风暴(大量线程同时超时/中断)时,每个取消节点都要向前遍历跳过已取消节点,导致 O(n) 的链式清理,加上大量 CAS 竞争 tail 和 head,吞吐显著下降,甚至可能让队列陷入频繁的唤醒与清理循环。

取消是 AQS 最复杂的路径之一。为降低取消风暴影响,应尽量避免大量线程同时超时(如错峰超时),并认识到取消路径的遍历成本随队列长度线性增长。

#
★★★

9. AbstractQueuedLongSynchronizer 与 AQS 的差异

AbstractQueuedLongSynchronizer(AQLS)与 AbstractQueuedSynchronizer(AQS)有何差异,各自适用什么场景?

  • state 字段的位宽差异(int vs long)
  • 语义上的本质相同
  • 适用场景(64 位状态计数)

AQLS 与 AQS 的排队、阻塞、唤醒机制完全相同,唯一区别是 state 字段的类型:AQS 用 int(32 位),AQLS 用 long(64 位)。因此 AQLS 适用于需要 64 位状态计数或标签的同步器,例如需要高精度计数器或把状态与版本号打包进 64 位的场景。JDK 未内置使用 AQLS 的公开同步器,但它是自定义同步器的备选基类。

两者的代码几乎镜像,只是把 state 的 CAS 从 int 换成 long。选择依据纯粹是状态位宽是否够用,而非功能差异。

#
★★★

10. AbstractQueuedSynchronizer(AQS)的 CLH 队列锁实现原理与 JDK 25 中的演化

请说明 AQS 基于 CLH 队列锁变体的实现原理,以及到 JDK 25 为止的相关演化?

  • CLH 变体的双向链表与 waitStatus
  • 入队 park 与唤醒机制
  • JDK 25 虚拟线程下 AQS 的协作(JEP 491 后的卸载)

AQS 的队列是 CLH 变体:每个等待线程一个 Node,Node 通过 prev/next 组成双向队列,waitStatus 记录 SIGNAL/CANCELLED/CONDITION/PROPAGATE。获取失败先入队,前驱为 head 时再尝试获取,否则 park 挂起;释放时唤醒严格的后继。JDK 25 相比早期版本,队列算法本身几乎不变,但增加了与虚拟线程的协作:当虚拟线程阻塞在 AQS 上时,LockSupport.park 会卸载载体线程,让载体线程去执行其他任务;JEP 491 消除了 synchronized 的钉住问题后,AQS 锁与虚拟线程的配合更顺畅。

理解 AQS 的 CLH 变体是理解所有显式锁的基础。JDK 25 的演化重点是让 AQS 的阻塞与虚拟线程的调度卸载协同,从而提升虚拟线程下的吞吐。

#
★★★

11. AbstractQueuedSynchronizer(AQS)的核心数据结构与状态位

AQS 的核心数据结构是什么?其内部等待节点的状态位(waitStatus)各代表什么含义?

  • Node 双向链表与 head/tail
  • state 字段与 CAS 语义
  • waitStatus 的四个取值含义

AQS 的核心数据结构是:一个 volatile int state 代表同步状态,一个 head/tail 指向的 Node 双向链表作为等待队列。Node 的 waitStatus 取值:CANCELLED=1(节点因超时或中断被取消)、SIGNAL=-1(后继节点需要被唤醒,即该节点释放后要唤醒后继)、CONDITION=-2(节点在条件队列中等待)、PROPAGATE=-3(共享模式下应继续传播唤醒)、0(初始状态)。state 通过 CAS(compareAndSetState)原子更新,head/tail 也是 volatile 的。

state 是同步器的"语义核心",取决于子类如何解释(0=未锁、1=已锁、可重入计数、计数许可等)。Node 队列解决了"获取不到时如何排队等待",waitStatus 则用于协调唤醒与取消。

#
★★★

12. CAS 在 AQS(AbstractQueuedSynchronizer)中的作用

CAS(Compare-And-Swap)在 AQS 中承担了哪些关键作用?

  • state 的原子更新(compareAndSetState)
  • 入队/出队时 head/tail 的 CAS
  • waitStatus 的 CAS 修改

CAS 在 AQS 中无处不在:入口处用 compareAndSetState 原子地尝试获取/释放锁(相当于 tryAcquire 的底层);入队时用 compareAndSetTail 把新节点追加到队尾;出队时用 compareAndSetHead 更新头节点;还能用 compareAndSetWaitStatus 修改节点状态。CAS 让 AQS 在无锁的情况下安全地维护共享状态,只有获取失败需要排队时才用 park 阻塞。

CAS 提供了"读-改-写"的原子性,避免了复杂的加锁,是 AQS 高性能与无锁实现的基石。当 CAS 失败时通过重试(自旋)或入队等待来协调竞争。

#
★★★

13. CAS 自旋与 ReentrantLock 各有哪些调度和吞吐影响,如何压测比较

CAS 自旋与 ReentrantLock 在调度和吞吐上各有什么影响?如何设计压测来比较二者?

  • 自旋的 CPU 空转与无阻塞特性
  • ReentrantLock 的 park/unpark 与上下文切换
  • 压测设计(竞争程度、线程数、临界区大小)

CAS 自旋:不阻塞线程,无 park/unpark 与上下文切换开销,但竞争激烈时多个线程同时空转抢占同一个缓存行,造成 cache line ping-pong 和 CPU 空耗;在低竞争且临界区极短时吞吐最高。ReentrantLock:竞争时线程 park 挂起,由 OS 调度,释放 CPU 让出处理器,但引入上下文切换与唤醒延迟,在高竞争、临界区较长时吞吐更稳定、尾延迟更可控。压测应控制三个变量:线程数(竞争度)、临界区执行时间(自旋划算与否)、加锁/释放频率,并测量吞吐(ops/s)与延迟分布(P50/P99),同时用 JFR 或 jstat 观察 CPU 使用率与上下文切换次数。

本质是"忙等"与"阻塞"的取舍。粗略经验:临界区 < 几十纳秒且竞争低时自旋胜,临界区大或竞争高时锁胜。压测一定要改变竞争度,单一竞争度下结论容易误导。

#
★★★

14. Condition.signal 为什么要求当前线程持锁,节点从条件队列转移到同步队列经历哪些步骤

为什么 Condition.signal 要求当前线程必须持有锁?节点从条件队列转移到同步队列要经历哪些步骤?

  • signal 持锁的必要性(保证条件队列与同步队列的一致性)
  • 节点转移(等待队列 → 同步队列)的完整过程
  • 丢失唤醒的防止

Condition.signal 要求持锁,是因为条件队列的维护(入队、出队、转移)与同步队列的 head 操作都需要在锁保护下进行,避免多个线程并发操作条件队列造成竞态。转移步骤:signal 时把条件队列头节点摘除,通过 enq 把该节点加入同步队列尾,并将该节点 waitStatus 从 CONDITION 改为 0(或 SIGNAL),然后根据决定是否唤醒执行 unpark 该线程。被唤醒的线程从 await 中恢复,重新尝试获取锁,获取成功后才真正返回。这个"先转移、再竞争锁"的流程保证了 await 的线程不会丢失唤醒信号。

由于条件队列的操作必须与锁的 state 变化保持原子一致,所以 signal/await 都要求持锁。若改在无锁时调用,可能造成节点被并发转移导致丢失唤醒。

#
★★★

15. CountDownLatch 与 CyclicBarrier 的差异(一次性 vs 可重用)

CountDownLatch 与 CyclicBarrier 有何差异?为什么前者是一次性的、后者可重用?

  • 触发语义:计数减到 0 vs N 个线程到齐
  • 可重用性:CyclicBarrier 的 reset 与 generation
  • 使用场景差异

CountDownLatch 是"倒计时":一个或多个线程调用 countDown 递减计数,计数归零时所有 await 的线程放行,计数归零后不可重置(一次性)。CyclicBarrier 是"屏障":N 个线程各自 await,全部到齐后同时放行,且会自动进入下一代(generation),可重复使用。语义上 CountDownLatch 用在"等待事件/计数完成",CyclicBarrier 用在"多个子任务相互等待会合"。CyclicBarrier 还支持 barrierAction 在屏障打开时执行。

一次性 vs 可重用源于内部实现:CountDownLatch 用 AQS 共享模式 state 递减,归零后不再恢复;CyclicBarrier 用 generation 轮次管理,每轮结束后 generation+1 并重置参与的计数。

#
★★★

16. CountDownLatch 在多模块启动等待的应用

CountDownLatch 在多模块启动等待场景中如何应用?有哪些容易踩的坑?

  • 主线程等待多个子模块初始化完成的模式
  • 计数与模块数的一致性
  • 超时与失败处理

典型用法:主线程创建 CountDownLatch(N),每个模块完成后调用 countDown,主线程 await() 等待所有模块就绪。例如连接池、缓存预热、RPC 注册等多个模块并行初始化,主线程等待全部完成后再接收流量。坑点:countDown 次数必须与 N 严格一致,多一次会提前放行、少一次会永久等待;应使用 await(timeout, unit) 带超时避免永久阻塞;失败模块要 countDown 并记录错误,否则主线程卡死;要把 countDown 放在 finally 中,防止异常导致计数不完整。

CountDownLatch 适合"主从"模型(一个主线程等 N 个从任务)。关键是把计数与模块数绑定准确,并加超时与 finally 兜底。

int modules = 4;
CountDownLatch latch = new CountDownLatch(modules);
for (int i = 0; i < modules; i++) {
    executor.submit(() -> {
        try { initModule(); } finally { latch.countDown(); }
    });
}
if (!latch.await(30, TimeUnit.SECONDS)) {
    throw new IllegalStateException("部分模块初始化超时");
}
#
★★★

17. CyclicBarrier 与 Phaser 在多阶段任务中的动态参与者管理差异

在多阶段任务中,CyclicBarrier 与 Phaser 在参与者动态管理上有什么差异?

  • CyclicBarrier 参与者固定(构造时指定)
  • Phaser 支持动态注册/注销
  • 阶段推进与 party 调整

CyclicBarrier 的参与者数量在构造时固定,无法中途增加或减少,因此不适合参与者动态变化的场景。Phaser 支持 register/bulkRegister 动态注册新参与者,以及 arriveAndDeregister 注销参与者,能够灵活应对任务数量在运行中变化的多阶段任务。此外 Phaser 天然支持多阶段推进(phase 递增),而 CyclicBarrier 每一轮都要重新构造或 reset。当参与者数量固定且阶段简单时,CyclicBarrier 更简单;当参与者动态变化或多阶段时,Phaser 更合适。

动态参与者管理是 Phaser 相对 CyclicBarrier 的核心优势,但也带来更高的复杂度与更易出错的操作(如忘记 deregister 会阻止推进)。

#
★★★

18. JMH 对 synchronized/ReentrantLock 的对比基准

如何用 JMH 设计一个公平的 synchronized 与 ReentrantLock 对比基准?

  • @State 与 @Benchmark 的使用
  • 控制竞争度与临界区大小
  • 预热与防抖(Fork、Warmup、Measure)

用 JMH 对比时要注意:用 @State 管理共享资源,避免 JIT 消除加锁;用不同线程数(@Threads)控制竞争度;临界区做少量真实工作(如对计数器做累加)避免被优化掉;用 @Fork(5)、@Warmup(iterations=5)、@Measurement(iterations=10) 充分预热。注意 synchronized 可能被锁消除/锁粗化优化,ReentrantLock 不会被优化,因此结论要谨慎。还应测量不同竞争度(低线程数 vs 高线程数)以得到完整曲线。

微基准的最大陷阱是 JIT 优化掩盖真实开销。加锁对象必须逃逸(@State 共享),临界区做有副作用的工作,且用多线程制造真实竞争,否则数据无意义。

@State(Scope.Benchmark)
public class LockBench {
    private final Object monitor = new Object();
    private final ReentrantLock lock = new ReentrantLock();
    private long count;
    @Benchmark @Threads(8)
    public long sync() { synchronized (monitor) { return ++count; } }
    @Benchmark @Threads(8)
    public long reentrant() { lock.lock(); try { return ++count; } finally { lock.unlock(); } }
}
#
★★

19. CyclicBarrier 的 barrierAction 与破损处理

CyclicBarrier 的 barrierAction 是什么?屏障破损(broken)时如何处理?

  • barrierAction 的执行时机
  • BrokenBarrierException 与 reset 恢复
  • 破损后的恢复(reset)

barrierAction 是构造时传入的 Runnable,当所有参与线程到达屏障(最后一方到达)时,在最后一个线程内执行一次,适合做"会合后的汇总/切换"。屏障破损指某个线程在等待中被中断、超时或屏障 action 抛异常,此时屏障进入 broken 状态,其余等待线程会收到 BrokenBarrierException。处理方式:捕获 BrokenBarrierException 后调用 reset() 使屏障回到初始代,或选择放弃。注意破损后所有线程都必须重新处理,否则会无限等待。

barrierAction 是"全部到齐后、放行前"的钩子;broken 是 CyclicBarrier 的失败语义,必须显式 reset 才能恢复,这比 CountDownLatch 更复杂。

#
★★

20. CyclicBarrier 的屏障动作失败后为何会进入 broken 状态,其他等待线程将收到什么信号

为什么 CyclicBarrier 的 barrierAction 抛异常会导致屏障进入 broken 状态?其他等待线程会收到什么信号?

  • broken 状态的触发条件
  • 失败传播的语义
  • 对其他线程的信号

当最后一个到达的线程执行 barrierAction 时,若该 action 抛异常,屏障无法正常放行,此时屏障被标记为 broken,以防止其余线程永远等待并造成状态不一致。其他等待线程要么已放行(在异常前到达的),要么在屏障下一次 await 时抛出 BrokenBarrierException。若屏障已经破损,随后到达的线程也会立即收到 BrokenBarrierException。这保证了某一方失败时所有相关方都能感知,而不是静默挂起。

broken 是 CyclicBarrier 的"失败即广播"机制,避免"部分线程成功、部分线程永久等待"的不一致。它牺牲了容错性换取明确失败语义。

#
★★

21. 虚拟线程阻塞在 ReentrantLock 上通常能卸载载体线程,JEP 491 前 synchronized 或外部原生调用何时仍会钉住载体线程

虚拟线程阻塞在 ReentrantLock 上时通常能卸载载体线程,那么在 JEP 491 之前,synchronized 或外部原生调用在什么情况下仍会钉住载体线程?

  • 虚拟线程的卸载机制与钉住(pinning)
  • synchronized 块内阻塞导致钉住
  • 原生调用(native)不释放载体线程

虚拟线程在阻塞时,若不占用载体线程的其他资源,可卸载载体线程。但 JEP 491 之前,虚拟线程在 synchronized 块/方法内执行阻塞操作(如 IO、park)时,会钉住载体线程——因为 synchronized 的 Monitor 与载体线程绑定,无法安全卸载。此外,调用外部原生方法(native)时,JVM 无法判定界限,也会钉住。钉住导致载体线程被某个虚拟线程占用,无法服务其他虚拟线程,从而降低并发吞吐。JEP 491 通过在虚拟线程阻塞时释放/重新获取 Monitor 来解决 synchronized 钉住;原生调用的钉住仍可能残留。

钉住(pinning)是虚拟线程与平台线程语义冲突的体现。JEP 491 解决了 synchronized 的钉住,但原生调用(如 JNI、某些外部库中的阻塞原生 IO)仍无法卸载载体线程。

#
★★

22. JDK 25 虚拟线程与 AQS 的协作边界

JDK 25 中虚拟线程与 AQS 是如何协作的?协作的边界在哪里?

  • 虚拟线程阻塞在 AQS 上时的卸载
  • LockSupport.park 的虚拟线程实现
  • JEP 491 后 AQS 语义不变

虚拟线程阻塞在 AQS 锁上时,AQS 内部调用 LockSupport.park,而 LockSupport 对虚拟线程有专门实现:park 会触发载体线程卸载(continuation 挂起),释放载体线程去执行其他虚拟线程。因此虚拟线程在 AQS 上等待锁不会占用载体线程,非常适合大量虚拟线程并发等待锁的场景。协作边界在于:AQS 的排队/唤醒语义完全不变,只是 park 的底层由"阻塞载体线程"变为"卸载虚拟线程";一旦虚拟线程被 unpark 唤醒,会重新挂载到某个载体线程上继续执行。

虚拟线程对 AQS 是"透明的":AQS 只看到 park/unpark,不感知虚拟线程。正是这种抽象,让虚拟线程在锁等待上实现了高并发卸载。

#
★★

23. JEP 491 消除 synchronized 虚拟线程钉住后,JDK 25 中选择 synchronized 与 ReentrantLock 应看哪些需求

JEP 491 消除了 synchronized 的虚拟线程钉住问题后,在 JDK 25 中应如何选择 synchronized 还是 ReentrantLock?

  • 钉住消除后的语义变化
  • synchronized 的简单性与 ReentrantLock 的扩展能力
  • 按需求选择(公平、中断、超时、Condition、tryLock)

若只需基本的互斥与可见性,synchronized 更简洁、无锁泄漏风险、JIT 可锁消除/锁粗化,且在非虚拟线程下性能与 ReentrantLock 相当;JEP 491 后它在虚拟线程下也不再钉住,因此默认场景优先用 synchronized。若需要可中断获取(lockInterruptibly)、超时(tryLock(timeout))、公平锁、多个 Condition 条件队列、非阻塞 tryLock 等高级能力,则用 ReentrantLock。选型依据是"需要哪些锁能力",而非性能。

JDK 25 中两者的性能差异已大幅缩小,synchronized 失去钉住劣势后,选择回归到"能力需求":内置锁胜在简单,ReentrantLock 胜在灵活。

#
★★

24. Java 9 ReentrantLock 的响应线程优先级调整

Java 9 在 ReentrantLock 上做了什么关于线程优先级调整的改动?

  • Java 9 对锁的优先级相关手段
  • 移除或调整线程优先级 API 的影响
  • 优先级与锁竞争的关系

Java 9 中对线程优先级相关的主要动作是:移除 Thread 的 stop 相关(与优先级无关),并强调锁获取顺序不依赖线程优先级。实际上 Java 9 起,AQS/ReentrantLock 的唤醒顺序完全由 FIFO 队列决定,线程优先级(setPriority)在大多数现代 OS 上不被 AQS 尊重,锁竞争结果不因优先级改变。因此不应依赖线程优先级来保证锁获取顺序或公平性,线程优先级只是给 OS 的调度提示。

线程优先级在 Java 中一直只是"提示",现代 JVM 与 OS 往往忽略它。锁竞争顺序以 AQS 队列为准,这是确定性的保证。

#
★★

25. Phaser 与 CyclicBarrier 的差异

Phaser 与 CyclicBarrier 有何差异?

  • 动态参与者数量
  • 多阶段推进
  • 终止与同步语义

主要差异:一是参与者数量,CyclicBarrier 构造时固定,Phaser 支持 register/arriveAndDeregister 动态增减;二是阶段推进,CyclicBarrier 每轮用 reset 或重建,Phaser 通过 phase 自动推进多阶段,且每个阶段可设定不同的参与方;三是 Phaser 提供了更细粒度的方法(arrive、awaitAdvance、arriveAndAwaitAdvance),并支持父子 Phaser 树;四是 Phaser 的计数基于每个阶段,不会因线程数变化而失效。代价是 Phaser 更复杂、更易出错。

当任务数固定且只有一轮会合时优先 CyclicBarrier(简单);多阶段或动态参与方时用 Phaser。Phaser 是 CyclicBarrier 的"超集"。

#
★★

26. Phaser 支持动态注册和分阶段推进,与 CyclicBarrier 相比在任务数量变化时应如何选择

当任务数量会在运行中变化时,应在 Phaser 与 CyclicBarrier 之间如何选择?

  • 动态注册 vs 固定参与方
  • 运行期增减并发的场景
  • 选择依据

当任务数量在运行中变化时,应使用 Phaser:它允许在任意阶段 register/bulkRegister 增加参与方,或 arriveAndDeregister 减少参与方,且每个阶段独立计数。而 CyclicBarrier 的参与方在构造时固定,运行中增减会破坏会合逻辑。因此,任务集合动态变化(如并行计算中按需分裂任务、动态加入/退出 worker)应选 Phaser;若参与方在生命周期内固定且逻辑简单,选 CyclicBarrier 更省心。

选择的核心是"参与方是否静态"。动态变化务必用 Phaser 并注意配对 register/deregister,否则会阻止屏障推进或终止。

#
★★

27. ReentrantLock 公平锁与非公平锁在吞吐与延迟上的取舍

ReentrantLock 的公平锁与非公平锁在吞吐和延迟上如何取舍?

  • 非公平锁的插队提前获取
  • 公平锁的 FIFO 与避免饥饿
  • 吞吐 vs 尾延迟

非公平锁:新线程在队列头部之前直接尝试 CAS 抢占,若成功则"插队"先行获取,减少了上下文切换与唤醒开销,吞吐通常更高,但可能让队列中的线程长期饥饿,尾延迟波动大。公平锁:严格按 FIFO 获取,新线程先检查 hasQueuedPredecessors,有前驱则排队,避免了饥饿,尾延迟更稳定,但每次获取都有排队检查、平均延迟略高、吞吐略低。取舍:追求吞吐且允许偶尔延迟抖动用非公平(默认);要求无饥饿、延迟稳定用公平。

非公平锁的吞吐优势来自"减少线程唤醒"——插队线程无需 park/unpark 即可拿到刚释放的锁。公平锁以少量吞吐换取确定性。

#
★★

28. ReentrantLock 基于 AQS 的公平/非公平实现差异

ReentrantLock 的公平锁与非公平锁基于 AQS 的实现差异在哪里?

  • 非公平锁的 tryAcquire 直接 CAS
  • 公平锁的 hasQueuedPredecessors 检查
  • 同一 AQS 状态机的不同钩子

差异集中在 tryAcquire 的实现。非公平锁(NonfairSync)的 tryAcquire 直接 compareAndSetState(0,1) 尝试抢占,不检查队列中是否有等待者;公平锁(FairSync)的 tryAcquire 先调用 hasQueuedPredecessors(),若队列中已有排队的先驱则返回 false,只有队列为空或自己是队首时才 CAS 获取。其余入队、park、唤醒逻辑完全由 AQS 提供,两种 Sync 只重写 tryAcquire 这一个钩子。

这体现了 AQS 模板方法设计的精妙:公平性差异只需改变"是否允许插队"这一处判断,其余复杂机制复用。

// 非公平
protected final boolean tryAcquire(int acquires) {
    if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; }
    // ... 可重入判断
}
// 公平
protected final boolean tryAcquire(int acquires) {
    if (hasQueuedPredecessors()) return false; // 有先来者则排队
    if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; }
    // ...
}
#
★★

29. ReentrantLock 的公平与非公平实现都基于 AQS 时,吞吐量、锁饥饿和队列竞争为何会不同

既然公平与非公平都基于 AQS,为什么吞吐量、锁饥饿和队列竞争会不同?

  • 插队与否影响唤醒次数
  • 上下文切换与缓存竞争的差异
  • 饥饿与竞争的反直觉关系

尽管底层队列相同,但非公平锁允许新线程插队,减少了"锁释放后唤醒队首线程"的次数(队首线程可能还在 park 中,插队线程瞬间拿到锁),从而减少上下文切换与缓存线竞争,吞吐更高。公平锁每次都要唤醒队首,每个线程都经历 park→unpark→竞争,上下文切换多,吞吐较低。锁饥饿则相反:非公平锁下队尾线程可能被反复插队而长期饥饿,公平锁 FIFO 保证无饥饿。竞争上,非公平锁在锁释放瞬间的 CAS 竞争更高(插队者与队首同时抢),但整体有效工作量更大。

关键在"是否等待唤醒":非公平锁牺牲队首的确定性换取更少的唤醒开销,因而吞吐高、饥饿风险高;公平锁确定性换取吞吐。

#
★★

30. ReentrantLock.tryLock 的应用模式

ReentrantLock.tryLock 的典型应用模式是什么?有哪些注意事项?

  • tryLock 非阻塞获取
  • tryLock(timeout) 超时获取
  • 失败处理与锁释放

tryLock 用于"非阻塞地尝试获取锁",成功返回 true,失败返回 false(不阻塞)。常用模式:tryLock() 成功则执行业务并必须 finally 释放,失败则走降级/重试/放弃路径,避免死锁等待。tryLock(timeout, unit) 则带超时等待,超时仍未获取返回 false,可中断。注意:tryLock 成功后必须 try/finally 中 unlock 防止泄漏;tryLock 不保证公平;失败时不能重复 unlock;中断时 tryLock(timeout) 会抛 InterruptedException。

tryLock 是"限时获取"和"避免锁等待造成阻塞"的关键工具,常用于多锁的获取顺序控制(避免死锁)与降级策略。

if (lock.tryLock(3, TimeUnit.SECONDS)) {
    try { doWork(); } finally { lock.unlock(); }
} else {
    // 降级或重试
    fallback();
}
#
★★

31. ReentrantReadWriteLock 的读锁、写锁状态共用

ReentrantReadWriteLock 如何把读锁计数与写锁状态共用在一个 int state 中?

  • 高 16 位/低 16 位拆分
  • 读锁计数与写锁可重入计数
  • 位运算与溢出风险

ReentrantReadWriteLock 的 Sync 用一个 int state 分成两部分:高 16 位表示"读锁持有总数"(含可重入的读锁),低 16 位表示"写锁持有数"(写锁独占,可重入时计数)。通过位运算拆取:读锁计数 = state >>> 16,写锁计数 = state & 0xFFFF。获取读锁用 CAS 对高 16 位加一,获取写锁用 CAS 对低 16 位加一。这种共享一个 state 让两种锁的互斥与可重入在单个原子变量上表达,简洁且无需额外字段。缺点是 16 位上限(65535),溢出会抛错误。

状态共用是读写锁实现的核心技巧:写锁存在时读锁必须为 0(互斥),读锁存在时写锁必须为 0(读-写互斥),在一个 int 上方便检查"读锁不为 0 或写锁不为 0"。

#
★★

32. ReentrantReadWriteLock 的读锁升级/降级问题

ReentrantReadWriteLock 为什么不允许读锁升级为写锁,却允许写锁降级为读锁?

  • 读锁升级导致死锁
  • 写锁降级的正确性
  • tryWriteLock 替代升级

ReentrantReadWriteLock 支持写锁降级(先取写锁再取读锁,然后释放写锁)但不支持读锁升级(先取读锁再取写锁)。升级会导致死锁:多个线程同时持有读锁,都尝试升级为写锁,但写锁需要所有读锁释放,互相等待永远无法完成。降级则是安全的:写锁独占时取读锁,之后释放写锁,读锁仍持有,保证一致的读视角。若需升级,应使用 tryWriteLock 或先释放读锁再获取写锁,并处理竞态。

升级是经典的"哲学家就餐"式死锁,Java 官方明确不允许。降级常用于缓存更新:先写数据,降级为读锁,保证后续读看到新数据。

#
★★

33. Semaphore 与 CountDownLatch 在资源池与一次性协调上的工程取舍

Semaphore 与 CountDownLatch 在资源池与一次性协调场景上的工程取舍是什么?

  • Semaphore 的可重复许可 vs CountDownLatch 的一次性归零
  • 资源池限流 vs 一次性事件等待
  • 选型依据

Semaphore 的许可可重复发放与回收,适合资源池、连接池限流、并发上限控制等"可循环"场景;CountDownLatch 的计数值不可逆地归零,适合"一次性事件/里程碑"等待,如等待多个初始化完成。工程取舍:若需要动态放行/回收、反复控制并发量,用 Semaphore;若只关心"某个一次性事件是否发生",用 CountDownLatch。误用场景:用 CountDownLatch 做限流(无法回收)或把 Semaphore 当一次性事件(语义不清)。

关键区别是"量"是否可逆。Semaphore 管理的是一个可增可减的许可证量,CountDownLatch 管理的是一个只减不增的倒计数。

#
★★

34. Semaphore 与 CountDownLatch 都使用共享同步思想时,前者可重复获取许可证而后者一次性归零,这对资源控制有何影响

Semaphore 与 CountDownLatch 都基于共享同步思想,但前者许可可重复获取、后者一次性归零,这对资源控制有何影响?

  • 共享模式的两种实现方向
  • 可重复性对资源控制的影响
  • 一次性对事件语义的影响

两者都基于 AQS 共享模式,但 Semaphore 的 state 表示"可用许可数",acquire 时减少、release 时增加,可反复使用,能动态控制资源池的并发量,适合限流与资源分配。CountDownLatch 的 state 表示"剩余计数",只减不增,归零后永久打开,适合一次性协调。资源控制上,Semaphore 可回收许可、可调整并发上限,能防许可证泄漏(配合 finally release);CountDownLatch 无法回收,一旦归零失去控制力。因此资源池用 Semaphore,一次性事件用 CountDownLatch。

共享模式只是"多线程可同时通过"的通用框架,具体资源控制语义由 state 的增减方向决定:可逆(Semaphore)vs 单向(CountDownLatch)。

#
★★

35. Semaphore 在连接池限流的应用

Semaphore 如何用于连接池限流?有哪些实现要点?

  • 许可数 = 连接数上限
  • acquire/release 配对
  • 超时与中断处理

用 Semaphore(连接池容量) 作为连接池的并发闸门:取连接前 acquire() 获取许可(无许可则等待),用完在 finally 中 release() 归还许可,从而限制同时使用的连接数。要点:用 tryAcquire(timeout) 带超时避免长期等待;在 finally 中 release 防止泄漏(否则许可永久减少导致池逐渐不可用);可通过 drainPermits/reducePermits 动态调整;结合连接池自身的借出/归还机制,双保险控制并发。

Semaphore 限流的价值是"把并发上限从全局冗余代码中抽象出来",配合 finally 保证回收,是数据库/HTTP 连接池限流的经典做法。

Semaphore permits = new Semaphore(10);
if (permits.tryAcquire(2, TimeUnit.SECONDS)) {
    try { Connection c = pool.borrow(); use(c); } finally { pool.return_(c); permits.release(); }
} else { throw new TimeoutException("无可用连接"); }
#
★★

36. Semaphore 的 drainPermits、reducePermits 与动态扩缩容怎样配合,如何防止许可证被永久泄漏

Semaphore 的 drainPermits、reducePermits 如何配合实现动态扩缩容?如何防止许可证被永久泄漏?

  • drainPermits 一次性取空
  • reducePermits 减少上限
  • 动态调整与 finally 回收

drainPermits() 会一次性取走所有剩余许可(用于快速清空并发),reducePermits() 可减少许可总数(缩容),配合 release() 增加可用许可(扩容),可动态调整并发上限。防止泄漏的关键:所有 acquire 成功后必须在 finally 中 release(或 try-with-resources 封装),保证许可回归;同时避免在 acquire 与 release 之间抛出不处理异常导致许可丢失。还可通过监控可用许可数(availablePermits)发现异常泄漏。

动态扩缩容 = 调整可用许可的增减;防泄漏 = 强制 finally 回收 + 监控。reducePermits 与 drainPermits 是 JDK 提供的限额管理工具,不当使用会造成许可总数异常。

#
★★

37. Semaphore 的 tryAcquire/acquireUninterruptibly 的差异

Semaphore 的 tryAcquire 与 acquireUninterruptibly 有何差异?

  • 非阻塞 vs 阻塞
  • 是否响应中断
  • 超时参数的变体

tryAcquire() 非阻塞尝试获取许可,返回 boolean;tryAcquire(timeout, unit) 阻塞至多 timeout 时间,超时返回 false,可响应中断(抛 InterruptedException)。acquire() 阻塞获取直到成功,可中断(抛 InterruptedException);acquireUninterruptibly() 阻塞获取但忽略中断,即使线程被 interrupt 也不会抛异常,直到获得许可。差异核心:tryAcquire 带超时/非阻塞,acquireUninterruptibly 无限等待且不响应中断。

选择依据是"容许多久等待"与"是否需要响应中断"。acquireUninterruptibly 适合不允许中断的业务,但需注意中断状态会被保留。

#
★★

38. Semaphore 的许可证(Permit)机制与公平模式

Semaphore 的许可证机制是如何工作的?公平模式与非公平模式有何不同?

  • 许可计数与 acquire/release
  • 公平模式的队列顺序
  • 非公平模式的插队影响

Semaphore 的许可数存在 AQS 的 state 中,acquire 时 CAS 减少许可(不足则排队等待),release 时 CAS 增加许可并唤醒等待者。公平模式下,acquire 先检查 hasQueuedPredecessors,有等待者则排队,保证按 FIFO 顺序获取许可,避免饥饿但吞吐略低;非公平模式下,新线程可直接抢占许可,即使有线程在排队,吞吐更高但可能饥饿。

许可证机制本质是"共享资源量"的计数;公平模式只是决定是否允许新线程插队。与 ReentrantLock 的公平性考量同理。

#
★★

39. Semaphore、CountDownLatch、CyclicBarrier 的适用边界

Semaphore、CountDownLatch、CyclicBarrier 各自的适用边界是什么?

  • 三者语义定位
  • 资源控制 / 事件等待 / 会合
  • 选型决策

Semaphore:控制并发访问量(资源池、限流),许可可重复回收,关注"同时能有多少个"。CountDownLatch:一次性事件倒计时,等待多个操作完成,关注"某件事是否发生"。CyclicBarrier:多个线程相互等待会合,一起放行继续,可重用+可选 barrierAction,关注"多线程是否到齐"。适用边界:需要限流/资源控制用 Semaphore;需要等待一次性完成信号用 CountDownLatch;需要多线程同步会合(并可重复)用 CyclicBarrier。

三者是 "量"、"事件"、"会合"三种不同语义。选型靠判断业务是哪种场景,而非性能。

#
★★

40. StampedLock 常被与 AQS 一起比较,但它并不继承 AQS;请说明其乐观读验证和不可重入特性带来的取舍

StampedLock 不继承 AQS,请说明其乐观读验证(validate)与不可重入特性带来的取舍?

  • 乐观读的验证机制
  • 不可重入的限制
  • 取舍分析

StampedLock 的乐观读(tryOptimisticRead)不获取锁,只返回一个版本戳(stamp),读完后用 validate(stamp) 检查版本是否变化,若未变则本次读有效,否则需重试或升级为悲观读。好处是读操作几乎无锁开销、吞吐极高。取舍:不可重入(同一线程不能重复获取写锁/读锁,否则死锁),因此不能用于递归加锁;平时只适用于基本数据结构;乐观读的 validate 要正确处理(可能读到中间态);不支持 Condition。它牺牲了可重入与复杂语义,换取极致的读性能。

乐观读是"先读后验"的乐观并发,适合读多写少、读操作可接受小概率重试的场景;不可重入限制了其使用场景,但换来更简单的状态机。

#
★★

41. StampedLock 的乐观读不是可重入锁,校验失败、线程中断和锁转换应如何正确处理

StampedLock 的乐观读不是可重入锁,那么校验失败、线程中断和锁转换应如何正确处理?

  • 乐观读 validate 失败的重试
  • 中断与锁方法(readLock 抛 InterruptedException)
  • 锁转换(tryConvertToWriteLock 等)

校验失败:乐观读 validate 返回 false 时,应重试乐观读、升级为 readLock(悲观读)或直接 tryConvertToWriteLock 转写锁,保证读到一致数据。线程中断:readLock()/writeLock() 可中断,会抛 InterruptedException;而乐观读不阻塞、不响应中断,需自行检查。锁转换:用 tryConvertToReadLock/tryConvertToWriteLock/tryConvertToOptimisticRead 在持有某锁时转换,返回新 stamp 或 0(转换失败)。注意解锁要用对应 stamp 的 unlock。

乐观读的"非确定性"必须用 validate 兜底;中断与转换要依赖具体方法语义,避免误用 stamp 释放不该释放的锁。

long stamp = lock.tryOptimisticRead();
double v = read();
if (!lock.validate(stamp)) { stamp = lock.readLock(); try { v = read(); } finally { lock.unlockRead(stamp); } }
#
★★

42. Thread.join() 与 CountDownLatch 在父子线程协调上的语义差异

Thread.join() 与 CountDownLatch 在父子线程协调上的语义差异是什么?

  • join 等待具体线程终止
  • CountDownLatch 等待计数事件
  • 灵活性与耦合差异

Thread.join() 让当前线程等待目标线程终止,语义是"等待这个线程结束",与线程强绑定,无法等待"部分完成"或"多个线程的多阶段"。CountDownLatch 等待的是计数归零事件,与具体线程解耦:子线程只要调用 countDown 即可,不要求终止,可控制"等待完成某批工作"而非"等待线程结束"。join 更简单直接但耦合线程对象;CountDownLatch 更灵活,适合等待任务完成而非线程死亡。

关键差异:join 等待"线程死亡",CountDownLatch 等待"计数归零"。线程池中无法 join(无 Thread 引用),CountDownLatch 更适用。

#
★★

43. synchronized 与 ReentrantLock 的功能对比(公平/中断/Condition)

从公平性、可中断、Condition 等角度对比 synchronized 与 ReentrantLock?

  • 公平锁支持
  • 可中断获取
  • Condition 条件队列

synchronized:内置锁,不可显式加公平策略;不可中断获取(除非用 wait 超时);只有一个隐式条件(wait/notify);不支持 tryLock 超时。ReentrantLock:支持公平/非公平;支持 lockInterruptibly 可中断获取、tryLock(timeout) 超时;支持多个 Condition 条件队列(精确唤醒);支持非阻塞 tryLock。功能上 ReentrantLock 全面胜出,但 synchronized 更简单且 JIT 可优化(锁消除/粗化)。

需要公平、中断、超时、多条件时用 ReentrantLock;否则优先 synchronized。这是经典选型题。

#
★★

44. CountDownLatch、CyclicBarrier、Semaphore 的底层实现如何复用 AQS 状态机?

CountDownLatch、CyclicBarrier、Semaphore 如何复用 AQS 状态机?各自如何解释 state?

  • 三者对 state 的不同解释
  • 共享模式/独占模式的使用
  • 各自 tryAcquire/tryRelease 的差异

三者都基于 AQS 共享模式。Semaphore 的 state 表示可用许可数,tryAcquireShared 判断许可是否足够并 CAS 减少,tryReleaseShared 增加许可。CountDownLatch 的 state 表示剩余计数,tryAcquireShared 判断计数是否归零(归零返回 1 放行),tryReleaseShared 递减计数。CyclicBarrier 不直接继承 AQS,而是用 ReentrantLock + Condition 实现(独特实现),但思想类似:用锁保护计数,条件变量等待最后一个到达。可见三者通过"对 state 的不同解释 + 共享模式的排队/唤醒框架"复用 AQS。

AQS 共享模式提供"多个线程可同时通过"的框架,state 的语义由子类自定义,这是三者差异的根源。CyclicBarrier 是例外,用 ReentrantLock+Condition 而非继承 AQS。

#
★★

45. 如何正确处理 AQS 获取操作的超时和中断,避免取消节点残留、丢失唤醒或业务线程无限等待

如何正确处理 AQS 获取操作的超时和中断,以避免取消节点残留、丢失唤醒或无限等待?

  • 超时/中断的取消路径
  • 节点清理与状态恢复
  • 避免信号丢失

使用带超时的获取(tryAcquireNanos/tryAcquireSharedNanos)和可中断获取(acquireInterruptibly),在超时或中断时走 cancelAcquire 取消节点,将节点 waitStatus 置为 CANCELLED 并把前驱的 next 指针跳过它,同时恢复中断状态。为避免丢失唤醒:取消后检查是否有后继需要唤醒,必要时 unpark 后继;避免无限等待:始终使用超时而非无限期获取。业务侧要正确处理 InterruptedException(恢复中断标志或重新抛出),不要在 finally 中错误地释放。

正确的超时/中断处理要求"取消节点 + 清理链表 + 恢复中断标志 + 防止丢唤醒"四件事齐备,否则会出现节点残留或挂死。

#
★★

46. 如何用 jcstress 与故障注入验证自定义 AQS 同步器的互斥、可见性、超时和中断语义

如何用 jcstress 与故障注入验证自定义 AQS 同步器的互斥、可见性、超时和中断语义?

  • jcstress 的并发测试框架
  • @Outcome 与 @State 验证不变量
  • 故障注入(超时、中断的边角)

jcstress 是官方并发压力测试框架,可对自定义同步器做黑盒验证:编写 @State 与 @Actor 测试,用 @Outcome 声明期望的不变量(如互斥性:同一时刻临界区只有一个线程进入),jcstress 会以多种线程交错与内存序组合运行并校验。故障注入方面,可人工构造超时(短 timeout)、中断(提前 interrupt)、重复 release 等边界输入,验证同步器不会死锁、不丢唤醒、不残留节点。还需配合 jcstress 的 -D 参数与多轮运行覆盖弱序。

jcstress 验证的是"任何交错下不变量都成立"。自定义 AQS 同步器的正确性测试必须交给 jcstress 这类工具,单元测试无法覆盖交错与内存序。

#
★★

47. 实现一次性门闩式 AQS 同步器时,序列化、取消和重复 release 的行为应如何定义

实现一次性门闩(one-shot latch)式 AQS 同步器时,序列化、取消和重复 release 的行为应如何定义?

  • 一次性门闩的 state 语义
  • 重复 release 的容忍
  • 序列化与取消语义

一次性门闩可类比 CountDownLatch(1):state=1 表示未打开,release 时 CAS 将 state 置 0 并唤醒所有等待者,之后保持 0。行为定义:重复 release 应幂等(第二次 release 不改变已打开的 state,也不报错);序列化时应把 state 序列化,恢复后状态正确(若已打开则一恢复就放行);取消(await 超时/中断)应只影响当前线程,节点取消后不破坏门闩状态,其他线程仍可被已打开的 state 放行。设计要点是保证"一次性且幂等"。

一次性门闩的关键是 release 的幂等性与取消的不干扰性。用 tryReleaseShared 在 state 已为 0 时返回 false 即可实现幂等。

class OneShotLatch extends AbstractQueuedSynchronizer {
    protected int tryAcquireShared(int ignored) { return getState() == 0 ? 1 : -1; }
    protected boolean tryReleaseShared(int ignored) {
        setState(0); return true; // 幂等:重复调用也安全
    }
    public void await() { acquireShared(1); }
    public void release() { releaseShared(1); }
}
#
★★

48. 排查 AQS 锁竞争时,应结合队列长度、持锁时间、阻塞线程栈和 JFR 事件建立哪些诊断证据

排查 AQS 锁竞争时,应结合队列长度、持锁时间、阻塞线程栈和 JFR 事件建立哪些诊断证据?

  • 队列长度与锁竞争程度
  • 持锁时间与临界区
  • jstack/sampled 线程栈与 JFR 事件

诊断证据包括:1) 锁队列长度(AQS 队列中等待线程数)反映竞争激烈程度;2) 持锁时间(临界区执行时长)判断是否临界区过大导致竞争;3) 阻塞线程栈(jstack/ThreadMXBean 采样)定位哪些线程在哪个锁点等待、锁点在什么代码路径;4) JFR 事件(jdk.JavaMonitorEnter、jdk.ThreadPark、锁竞争相关事件)量化锁等待时长与次数。综合分析:队列长 + 持锁时间长 → 临界区过大或锁粒度粗;队列长 + 持锁时间短 → 锁点太热、竞争率高。据此决定降锁粒度、缩短临界区或改用无锁。

单看一个指标无法定位,需"队列长度 + 持锁时间 + 栈 + JFR"交叉验证。JFR 提供低开销的采样,是生产环境首选的诊断手段。

#
★★

49. 自定义 AQS 同步器的关键重写点

自定义 AQS 同步器需要重写哪些关键方法?各自的契约是什么?

  • tryAcquire/tryRelease(独占)
  • tryAcquireShared/tryReleaseShared(共享)
  • isHeldExclusively

独占模式重写 tryAcquire(返回 true 表示成功并设置 state)与 tryRelease(返回 true 表示释放成功可唤醒后继);共享模式重写 tryAcquireShared(返回正数=有剩余许可、0=已满、负数=失败需排队)与 tryReleaseShared(返回 true 表示要求唤醒后继)。此外可重写 isHeldExclusively 供 Condition 使用。关键契约:以原子方式更新 state,失败时无副作用;返回值语义必须准确,否则排队/唤醒逻辑错误。通常还需实现公平性(hasQueuedPredecessors 检查)。

重写的是"如何改变 state 达成语义",AQS 负责排队、阻塞、唤醒框架。返回值语义与无副作用是正确性核心。

#
★★

50. 虚拟线程在 AQS 锁上排队时如何卸载载体线程,持锁期间执行阻塞 I/O 仍有哪些风险

虚拟线程在 AQS 锁上排队时如何卸载载体线程?持锁期间执行阻塞 I/O 仍有哪些风险?

  • 虚拟线程 park 时的载体卸载
  • 持锁期间阻塞 I/O 的钉住风险
  • 载体线程被长时间占用

虚拟线程在 AQS 上排队时,LockSupport.park 会触发虚拟线程卸载——载体线程被释放去执行其他虚拟线程,因此大量虚拟线程等锁不会占用载体线程。但风险在于:若虚拟线程持有锁期间执行阻塞 I/O(在 synchronized 块内或 JEP 491 未覆盖的原生调用),会钉住载体线程,导致该载体线程无法服务其他虚拟线程,吞吐下降。此外,若持锁期间执行长时间计算或阻塞,会延长其他虚拟线程的等待,即使它们能卸载,整体仍串行受限。

卸载机制解决"等锁"的占用,但"持锁时阻塞"若发生在原生/未被 JEP 491 覆盖的路径,仍会钉住载体线程。因此应避免在持锁的 synchronized 临界区内做阻塞 I/O。

#
★★

51. 非公平 ReentrantLock 的插队路径与公平锁的 hasQueuedPredecessors 检查如何影响吞吐和尾延迟

非公平锁的插队路径与公平锁的 hasQueuedPredecessors 检查如何影响吞吐和尾延迟?

  • 插队减少唤醒
  • hasQueuedPredecessors 的排队检查成本
  • 尾延迟分布差异

非公平锁的插队路径:新线程直接 CAS 抢占,无需等待队首唤醒,减少了 park/unpark 与上下文切换,吞吐更高,但个别线程可能被反复插队(尾延迟大、抖动)。公平锁的 hasQueuedPredecessors 检查:每次获取都遍历/判断队列是否有前驱,增加了确定性的检查开销,平均延迟略高,但所有线程按 FIFO 获得锁,尾延迟稳定、无饥饿。因此公平锁以少量吞吐换尾延迟确定性。

吞吐与尾延迟这两个指标在公平/非公平上呈反向:非公平吞吐高但尾延迟差,公平反之。按业务对尾延迟的敏感度选择。

#
★★

52. Condition 在生产者-消费者中与 wait/notify 的等价转换与性能比较

Condition 在生产者-消费者中如何与 wait/notify 等价转换?两者性能如何?

  • wait/notify 与 Condition.await/signal 的对应
  • 精确唤醒 vs 全量唤醒
  • 性能差异

等价转换:Object.wait 对应 Condition.await,Object.notify 对应 signal,notifyAll 对应 signalAll(notify 与 signal 语义有微小差异,signal 保证唤醒一个)。性能上,Condition 更优:每个 Condition 有独立等待队列,可精确唤醒(如容器满时唤醒生产者、空时唤醒消费者),避免 notifyAll 唤醒所有线程导致的"惊群"与无效竞争;条件队列与 AQS 同步队列协作更高效。wait/notify 只有一个隐式条件,生产者消费者常需伪造两个锁或多个条件,易出错且 notifyAll 开销大。

Condition 的精确唤醒是相对 wait/notify 的核心优势。实现上 Condition 用独立条件队列,signal 只唤醒特定条件队列中的线程,减少无效唤醒。

#
★★

53. Condition 接口的精确唤醒与虚假唤醒

Condition 接口如何实现精确唤醒?如何处理虚假唤醒(spurious wakeup)?

  • 独立条件队列与 signal 语义
  • 虚假唤醒的存在
  • await 必须用 while 循环

Condition 的精确唤醒:每个 Condition 实例维护独立的条件等待队列,signal 只把该条件队列的头节点转移到同步队列并唤醒,精确唤醒"等待该条件"的线程,signalAll 唤醒条件队列中所有线程。虚假唤醒:即使没有 signal 调用(或因为 OS 限制),await 也可能返回,因此 await 返回后必须用 while 循环重新检查条件,不能只用 if。这是生产者-消费者正确性的关键。

精确唤醒降低无效唤醒,而虚假唤醒提醒 await 之后必须 while 重查条件。两者都是写并发代码的基础。

lock.lock();
try {
    while (queue.isEmpty()) notEmpty.await(); // 必须 while
    item = queue.poll();
} finally { lock.unlock(); }
#
★★

54. Java 8 StampedLock 与内存序的细节

Java 8 引入的 StampedLock 在内存序(内存可见性)上有哪些细节?

  • 乐观读的 volatile 读
  • writeLock 的释放与 acquire 语义
  • 内存序与可见性保证

StampedLock 通过在写锁释放时对 state 做 volatile 写、乐观读时对 state 做 volatile 读来建立可见性:乐观读 validate(stamp) 若通过,说明写锁期间没有修改 state,则读到的数据是"一致快照"(写锁内写入在释放时已发布)。悲观读 readLock 与写锁 writeLock 的获取/释放遵循 JMM 的锁语义(acquire/release 内存序)。StampedLock 使用 VarHandle 或 Unsafe 的 fence 在关键点保证内存序,确保乐观读校验与数据一致性。

乐观读的"零锁"是有代价的:它依赖 state 版本号的 volatile 可见性来判定读期间是否有写。内存序细节决定了 validate 的正确性。

#
★★

55. Phaser 如何支持参与者动态注册和注销,忘记 arriveAndDeregister 会导致何种无法终止问题

Phaser 如何支持参与者动态注册和注销?忘记 arriveAndDeregister 会导致什么问题?

  • register/arriveAndDeregister
  • 参与者计数与终止
  • 忘记注销的后果

Phaser 通过 register()/bulkRegister() 动态增加参与者,到达时用 arrive(),若需不再参与当前及后续阶段用 arriveAndDeregister()(到达并注销,减少后续阶段计数)。若忘记调用 arriveAndDeregister(只用 arrive 而不再等待),该参与者会一直计入后续阶段的到达数,导致后续阶段永远无法满足"所有参与者到达"而无法推进(phase 无法前进),进而整组任务无法终止,永久阻塞。因此注销必须与实际离开的线程配对。

动态参与者的核心是"注册与注销配对"。漏注销会造成永久无法推进,这是 Phaser 最容易踩的坑。

#
★★

56. Phaser 的注册表与阶段计数

Phaser 的注册表(parties)与阶段计数(phase)是如何工作的?

  • parties 与 unarrived 计数
  • phase 阶段推进
  • state 的位布局

Phaser 用一个 long state 编码:低 16 位未到达数(unarrived)、中 16 位参与者数(parties)、高 32 位阶段数(phase)。每有一个参与者到达,unarrived 减一;当 unarrived 归零时,该阶段完成,phase 递增,同时 unarrived 重置为 parties(下一阶段开始)。register 增加 parties,arriveAndDeregister 到达并减少 parties。这种位布局让"到达、推进、注册"都能用 CAS 原子完成。

Phaser 的状态编码在一个 long 中,用位运算实现多字段的原子更新与推进,是动态参与者的基础。

#
★★

57. StampedLock 的乐观读与失败重试模式

StampedLock 的乐观读与失败重试(retry)模式是怎样的?

  • tryOptimisticRead 获取 stamp
  • validate 校验
  • 失败后的重试/升级策略

模式:stamp = tryOptimisticRead()(不阻塞,返回版本号);读数据;validate(stamp) 校验版本是否变化。若 validate 返回 true,本次读有效,直接使用;若 false,说明期间有写,需重试乐观读或升级为悲观 readLock(再次读取,finally 释放)。可在循环中重试乐观读若干次,仍失败则升级悲观读,平衡了"尽量无锁"与"保证读到一致数据"。

乐观读是"先用后验"的乐观策略,失败重试或升级保证正确性。适合读多写少、读操作可重试的场景。

long stamp = lock.tryOptimisticRead();
int x = readX();
if (!lock.validate(stamp)) {
    stamp = lock.readLock();
    try { x = readX(); } finally { lock.unlockRead(stamp); }
}
#
★★

58. Condition 的等待队列与 AQS 同步队列如何协作,signal 与 signalAll 的区别?

Condition 的等待队列与 AQS 同步队列如何协作?signal 与 signalAll 有何区别?

  • 条件等待队列与同步队列的关系
  • signal 与 signalAll 的唤醒范围
  • 协作流程

Condition 每个实例维护一个 FIFO 条件等待队列(firstWaiter/lastWaiter)。await 时当前线程把节点加入条件队列并释放锁(state 置 0),然后 park;signal 把条件队列头节点转移到 AQS 同步队列(enq)并还原状态,该线程重新竞争锁。signalAll 把所有条件队列节点都转移到同步队列。区别:signal 只唤醒一个等待线程(条件队列头),signalAll 唤醒所有,适合难以判断唤醒哪个或条件可能被多个线程满足的场景。转移后所有线程都要重新获取锁。

条件队列是"等待条件的休眠队列",同步队列是"竞争锁的排队队列",signal 把前者迁移到后者。协作的关键是释放锁 → 休眠 → 转移 → 重新抢锁。

#

59. JDK 25 中虚拟线程与 ReentrantLock 的协作及 JEP 491 后的残余钉住风险

JDK 25 中虚拟线程与 ReentrantLock 如何协作?JEP 491 后还有哪些残余钉住风险?

  • 虚拟线程在 ReentrantLock 上的卸载
  • JEP 491 消除 synchronized 钉住
  • 残余钉住(原生调用、System 调用)

JDK 25 中虚拟线程阻塞在 ReentrantLock 上时,park 会卸载载体线程,不占用平台线程,因此大量虚拟线程等锁是高效的。JEP 491 解决了虚拟线程在 synchronized 块内阻塞时的钉住问题(改为可卸载)。但残余钉住风险仍在:调用外部原生方法(JNI)、某些阻塞的原生系统调用、signal 处理等 JVM 无法判定边界的地方,虚拟线程仍可能钉住载体线程;continuation 在原生栈上无法安全挂起。因此要避免在原生调用路径上做长阻塞。

JEP 491 是里程碑,但原生调用是 JVM 无法自动解决的边界,需人工避免在原生路径长阻塞。

#

60. wait()/notify() 与 Condition 在生产者-消费者模式中的语义差异

wait()/notify() 与 Condition 在生产者-消费者模式中的语义差异是什么?

  • 单一条件 vs 多条件
  • notifyAll 惊群 vs 精确唤醒
  • 使用约束

wait/notify 只能关联一个隐式条件,生产者-消费者通常需要"满"与"空"两个条件,不得不用一个锁配合 notifyAll,导致惊群(唤醒所有线程,多数无效竞争)。Condition 支持多个独立条件队列(如 notFull/notEmpty),可精确唤醒,减少无效竞争。此外 wait/notify 必须在 synchronized 块内使用、可能丢失唤醒;Condition 基于 ReentrantLock,更灵活。语义上两者都要求"等待条件用 while 循环"。

核心差异是"条件数量"与"唤醒精确度"。多线程多个等待条件时用 Condition 更清晰、更高效。

#

61. JDK 25 中 StampedLock 的乐观读模式在高读低写场景下的性能优势

JDK 25 中 StampedLock 的乐观读模式在高读低写场景下的性能优势是什么?

  • 乐观读零锁
  • 高读低写的吞吐
  • 与其他锁的对比

乐观读模式(tryOptimisticRead)不获取任何锁,只读 state 版本号后直接读数据,读操作几乎无同步开销,无 cache line 竞争、无阻塞。在高读低写场景下,读的吞吐接近无锁,远高于 ReentrantReadWriteLock(悲观读锁有原子计数与竞争)。缺点是写操作(取写锁)会使乐观读失效,需重试/升级。因此高读低写、读操作可重试时,StampedLock 乐观读是吞吐最优选择。

乐观读把"读"变为 O(1) 的版本检查,几乎没有争用,是读多写少场景的吞吐之王,代价是正确性需 validate 兜底。

#

62. StampedLock 的三种模式(写/悲观读/乐观读)在 JDK 25 中的内存语义

StampedLock 的写、悲观读、乐观读三种模式在 JDK 25 中分别具有什么内存语义?

  • 写锁的 release/acquire
  • 悲观读锁的共享读语义
  • 乐观读的 validate 语义

写锁 writeLock:获取是 acquire 语义、释放是 release 语义,与普通锁一致,写锁内写入在释放后对其他线程可见。悲观读 readLock:读锁可多线程共享,同样遵循 acquire/release 内存序,读锁内读取可见之前写锁的写入。乐观读 tryOptimisticRead:不获取锁,仅读取 state 版本,validate(stamp) 通过则说明读期间无写,读到的数据是写锁内发布的一致快照;内存语义依赖对 state 的 volatile 读与已建立的 happens-before。JDK 25 中这些语义通过 VarHandle 的 acquire/release/plain 访问实现。

三种模式的内存语义覆盖了"独占写、共享读、无锁乐观读"三种精度,乐观读的可见性由 validate 版本校验保证。

#

63. 手写一个基于 AQS 的限流器(如 Semaphore 变体)需要考虑什么?

手写一个基于 AQS 的限流器(如 Semaphore 变体)需要考虑哪些要点?

  • 共享模式与 state 语义
  • 许可获取/释放的原子性
  • 公平性、中断、超时、取消

需要继承 AQS 并重写 tryAcquireShared(判断许可是否足够并 CAS 减少,返回剩余许可数)与 tryReleaseShared(CAS 增加许可并返回是否需唤醒)。考虑要点:1) state 守恒(acquire 与 release 必须配对,防止泄漏);2) 非公平时直接抢占,公平时检查 hasQueuedPredecessors;3) 支持超时(tryAcquireNanos)与中断(acquireInterruptibly);4) 处理取消节点的清理;5) 决定是否支持动态调整上限(类似 reducePermits)。可在此基础上加流控参数(如每秒速率)扩展为令牌桶。

手写限流器的核心是"对 state 的加减与排队唤醒",复用 AQS 共享模式框架,重点保证 state 不泄漏与语义正确。

class RateLimiter extends AbstractQueuedSynchronizer {
    final int permits;
    protected int tryAcquireShared(int n) {
        for (;;) { int cur = getState(); int next = cur - n;
            if (next < 0) return -1;
            if (compareAndSetState(cur, next)) return next;
        }
    }
    protected boolean tryReleaseShared(int n) {
        for (;;) { int cur = getState(); int next = cur + n;
            if (compareAndSetState(cur, next)) return true;
        }
    }
}