资讯详情

34 面试官问你 AQS,其实是在看你会不会读 JDK 源码

📅 2026/11/5 3:51:06 | 华诺云谱 👁 阅读
34 面试官问你 AQS,其实是在看你会不会读 JDK 源码
面试现场AQS 是妥妥的高水位线问到这儿基本在测你的技术天花板。面试官ReentrantLock 用过吧它底层靠什么实现的候选人A靠 AQS全称 AbstractQueuedSynchronizer。面试官那你说说 AQS 里有个 state它是干嘛的候选人Astate 就是……一个状态变量记录锁的状态。面试官它等于 1 和等于 3 分别代表什么还有AQS 里那个队列到底是什么队列候选人A队列……就是等锁的线程排队吧具体没细看。候选人B接上state 是个 volatile int语义由子类定。ReentrantLock 里它表示重入次数——0 是没人拿锁1 是拿了一次3 就是重入了三次。队列是 CLH 锁队列的变体一个双向链表节点里存着线程和 waitStatus靠LockSupport.park/unpark阻塞和唤醒。整个 AQS 就是一个 int 加一个链表加上一套模板方法。面试官行那你把acquire走一遍。一、AQS 就两样东西一个 int一条链AbstractQueuedSynchronizer在java.util.concurrent.locks包下。别看它方法一长串核心数据结构就两样// AQS 的两个核心字段简化 private volatile int state; // 同步状态 private transient volatile Node head; // 队列头 private transient volatile Node tail; // 队列尾state 是灵魂。它是个volatile int但它的含义完全由子类定义-ReentrantLock里state 锁的重入次数0 空闲n 表示被重入了 n 次。-CountDownLatch里state 还剩几个倒计时没到。-Semaphore里state 当前可用的许可数。-ReentrantReadWriteLock里state 高低 16 位分别记录读锁和写锁的持有数。围绕 state 有三个 protected 方法全是 CAS 味道protected final int getState() { return state; } protected final void setState(int newState) { state newState; } protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }compareAndSetState是保证只有一个线程能抢到锁的关键——多个线程同时 CAS 从 0 改到 1只有一个成功其余的失败后进入等待队列。二、队列节点 Node线程的候车票AQS 的等待队列学名叫CLH 队列取自三位作者 Craig、Landin、Hagersten 的姓氏首字母。原始的 CLH 是个自旋锁队列AQS 用的是它的变体显式双向链表 park阻塞不自旋。每个等在锁外的线程会被包装成一个Nodestatic final class Node { // 标记节点处于共享模式还是独占模式 static final Node SHARED new Node(); static final Node EXCLUSIVE null; // waitStatus 的取值 static final int CANCELLED 1; // 线程已取消超时/中断 static final int SIGNAL -1; // 后继节点需要被唤醒 static final int CONDITION -2; // 节点在条件队列里等待 static final int PROPAGATE -3; // 共享模式下需要向后传播唤醒 volatile int waitStatus; volatile Node prev; // 前驱 volatile Node next; // 后继 volatile Thread thread; // 这个节点代表的线程 Node nextWaiter; // 条件队列里的下一个等待者 }四个waitStatus要记牢面试官经常挑SIGNAL问-SIGNAL(-1)最常用。表示我这个节点释放锁时得唤醒我的后继节点。-CANCELLED(1)线程等超时了或被中断了放弃排队。-CONDITION(-2)这个节点待在Condition的条件队列里不在同步队列上。-PROPAGATE(-3)只在共享模式下用表示唤醒要往后传播doReleaseShared里的场景。队列是双向的head是个虚节点dummy不存实际线程它只是方便操作的哨兵。真正排队的线程挂在head.next开始往后。三、acquire 全过程抢不到就排队排到了再自旋看 JDK 8 的acquire源码以下源码来自 JDK 8 java.util.concurrent.locks.AbstractQueuedSynchronizerpublic final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }一行一行拆1.tryAcquire(arg)先试着抢锁。这是个空实现抛UnsupportedOperationException交给子类写模板方法模式。ReentrantLock在里面判断 state 是不是 0、能不能 CAS。抢到了整个acquire直接返回。2. 抢不到走右边addWaiter(Node.EXCLUSIVE)把当前线程包成一个独占模式的节点用 CAS 挂到队列尾。3.acquireQueued(节点, arg)让这个节点排队等。它会自旋只有当前节点是head.next也就是说自己排在队首时才再试一次tryAcquire成功就把它设为新的 head出队。要是还不行就调shouldParkAfterFailedAcquire判断该不该park该睡就LockSupport.park睡过去。4.selfInterrupt()如果排队过程中线程被中断过这里补一次中断标记。因为park醒来时中断状态会被清掉得还回去。shouldParkAfterFailedAcquire里那段逻辑也很经典private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws pred.waitStatus; if (ws Node.SIGNAL) return true; // 前驱已是 SIGNAL可以安全 park if (ws 0) { // 前驱被 CANCELLED 了 do { node.prev pred pred.prev; // 一直往前找跳过取消的节点 } while (pred.waitStatus 0); pred.next node; } else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 把前驱标成 SIGNAL } return false; }它在干两件事一把前驱的 waitStatus 设成SIGNAL意思是你释放锁时记得叫我二顺手清理掉前面CANCELLED的僵尸节点。只有确认前驱是SIGNAL了才会放心去park——不然万一睡下去没人叫就永久阻塞了。release唤醒后继释放锁走release以下源码来自 JDK 8 AbstractQueuedSynchronizerpublic final boolean release(int arg) { if (tryRelease(arg)) { // 子类实现真正释放 Node h head; if (h ! null h.waitStatus ! 0) unparkSuccessor(h); // 唤醒 head 的后继 return true; } return false; }tryRelease返回true说明锁彻底空了对ReentrantLock而言就是 state 减到 0这时就调unparkSuccessor去唤醒队列里下一个还活着的线程。四、独占 vs 共享两种排队模式AQS 支持两套模式区别只在于同一时刻允不允许一个以上的线程通过模式子类要实现典型实现独占ExclusivetryAcquire/tryReleaseReentrantLock、ReentrantReadWriteLock 的写锁共享SharedtryAcquireShared/tryReleaseSharedCountDownLatch、Semaphore、读写锁的读锁-独占一次只放一个线程过。队列里的节点用Node.EXCLUSIVE标记。-共享一次可以放一批线程过比如 Semaphore 放行 3 个许可。进队列用Node.SHARED标记。tryAcquireShared的返回值有讲究负数表示失败0 表示成功但后续兄弟拿不到正数表示成功且后续还有富余。所以共享模式成功后会顺藤摸瓜地继续唤醒后面的共享节点直到许可用尽——这就是PROPAGATE状态存在的意义。独占模式下就算你唤醒了后继对方也得重新tryAcquire抢不到继续睡共享模式下前面的释放会尽量把后面能一起过的都放行。五、条件队列 ConditionCondition是 AQS 的另一半——它把synchronized里的wait/notify用显式的Object方法抽了出来变成await/signal/signalAll。每个ConditionObject内部维护一条条件队列。当线程调await()时1. 把当前线程包成节点waitStatus设成CONDITION挂进条件队列注意不是同步队列。2. 完全释放锁fullyRelease重入几次就减几次。3.park自己睡过去。当另一个线程调signal()1. 从条件队列里取一个节点。2. 把它从条件队列转移到同步队列尾部重新排队抢锁。3. 等它拿到锁后await()才真正返回。关键点await 时线程释放了锁被唤醒后要重新排队抢锁。这两条队列——同步队列和条件队列——是分开的节点会在两者之间来回搬家。六、ReentrantLock 怎么搭 AQS 的车讲完 AQS看ReentrantLock就一目了然了。它内部有个Sync继承 AQS再分出公平和不公平两个子类以下源码来自 JDK 8 java.util.concurrent.locks.ReentrantLockabstract static class Sync extends AbstractQueuedSynchronizer { // 非公平版抢锁 final boolean nonfairTryAcquire(int acquires) { final Thread current Thread.currentThread(); int c getState(); if (c 0) { // 没人拿锁 if (compareAndSetState(0, acquires)) { // CAS 直接抢 setExclusiveOwnerThread(current); return true; } } else if (current getExclusiveOwnerThread()) { // 是我自己拿的 int nextc c acquires; // 重入state 1 if (nextc 0) throw new Error(Maximum lock count exceeded); setState(nextc); // 这行不用 CAS因为持锁的就是自己 return true; } return false; } // 释放锁 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); // 减重入次数 return free; } }重入的精髓就在这两段拿锁时 state 加一释放时 state 减一减到 0 才算真正释放。所以同一个线程可以反复lock()只要lock()和unlock()次数对上就行。也正是因为state 0时持锁线程是当前线程那段setState(nextc)不需要 CAS。公平与非公平的差别只在tryAcquire里一行判断// 非公平直接抢抢不到再排队 final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }// 公平抢之前先看有没有人在排队有前驱就不抢 protected final boolean tryAcquire(int acquires) { final Thread current Thread.currentThread(); int c getState(); if (c 0) { if (!hasQueuedPredecessors() compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // ... 重入逻辑同 nonfairTryAcquire return false; }hasQueuedPredecessors()就是公平与非公平的分水岭。公平锁每次都问一句队里有人等吗有人在等就去排队杜绝插队非公平锁上来就 CAS 硬抢管你队里有没有人——所以非公平锁吞吐更高减少线程切换代价是可能饿死排队久的线程。 面试官真正想听的答案问 AQS他想看你能不能把一个 int 一条链讲成一套完整的并发原语。第一层给结论AQS 用一个 volatile int 的 state 表示同步状态、一条 CLH 双向队列管理等待线程用模板方法把怎么抢留给子类tryAcquire/tryRelease、tryAcquireShared/tryReleaseShared自己实现通用排队逻辑。第二层讲机制acquire先tryAcquire失败就addWaiter入队acquireQueued里自旋 LockSupport.parkrelease里tryRelease成功后unparkSuccessor唤醒后继。节点用SIGNAL/CANCELLED/CONDITION/PROPAGATE表达状态shouldParkAfterFailedAcquire负责把前驱标成 SIGNAL 并清理僵尸节点。第三层讲落地ReentrantLock用 state 记重入次数拿锁 1、释放 -1、到 0 才算释放公平锁靠hasQueuedPredecessors拒绝插队吞吐低、不饿死非公平锁直接 CAS 抢吞吐高、可能饥饿Condition用条件队列实现 await/signal节点在两条队列间搬家。能把这套串起来AQS 这一关就算过了。下篇预告AQS 里到处是compareAndSetState这个 CAS 到底是什么它凭什么不加锁就能保证原子性CAS 有个著名的 ABA 问题又怎么解AtomicInteger和LongAdder在高并发下谁更快、为什么下篇聊 CAS 与原子类从Unsafe到AtomicStampedReference再到LongAdder的分段思想把 JUC 的基石讲透。唠点键盘之外的 · 第 34 篇
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑