Java锁AQS框架原理分析:从状态管理到线程唤醒

📅 发布时间:2026/9/11 18:46:53
Java锁AQS框架原理分析:从状态管理到线程唤醒
多个线程争抢一把锁时成功的线程进入临界区失败的线程等待。这句话看起来简单实现起来却有不少问题等待线程放在哪里什么时候可以休眠释放锁时通知谁线程等待到一半被中断又该如何退出如果每实现一种锁都要重新处理这些问题并发工具的开发成本会很高。AQSAbstractQueuedSynchronizer抽象队列同步器把排队、阻塞、唤醒和取消等待这些公共机制封装起来让具体同步器主要负责定义“什么情况下允许通过”。一、AQS 在 Java 并发体系中处于什么位置开发者通常直接使用ReentrantLock而不是直接调用 AQS。ReentrantLock内部通过继承 AQS 的同步器实现锁的行为。可以把这种关系理解为业务代码│ 调用 lock / unlock▼ReentrantLock│ 内部 Sync 定义获取、释放规则▼AbstractQueuedSynchronizer│ 维护同步队列处理等待和取消▼原子操作 LockSupport.park / unparkAQS 是同步器框架并不局限于互斥锁。不同工具可以用同一个整数状态表达不同含义同步工具获取模式state的典型含义ReentrantLock独占锁的重入次数0 表示未持有Semaphore共享可用许可数CountDownLatch共享尚未完成的计数ReentrantReadWriteLock独占与共享将整数拆分为读、写计数并配合额外字段记录线程信息当然也不要把所有 Java 锁都归入 AQS。synchronized由 JVM 的监视器等机制实现并不是在内部调用 AQS。二、先理解分工子类定规则AQS 管等待AQS 使用模板方法式的设计。子类实现少量钩子方法AQS 在获取和释放流程中调用它们。子类方法负责回答的问题tryAcquire(int arg)当前线程能否独占获取成功时完成状态修改tryRelease(int arg)执行本次释放后是否已经完全释放tryAcquireShared(int arg)当前共享获取能否成功tryReleaseShared(int arg)本次共享释放是否可能使等待者成功isHeldExclusively()当前线程是否独占持有主要供条件变量使用子类按需要实现这些方法不必全部实现。没有实现的钩子默认会抛出UnsupportedOperationException。钩子本身必须保证线程安全通常应短小且不阻塞。以独占获取为例逻辑可以概括成尝试获取├─ 成功 → 返回└─ 失败 → 进入等待机制├─ 条件允许时重试├─ 暂时不能获取时阻塞└─ 按接口约定处理超时与中断三、state、CAS 和持有者分别负责什么1、state 是同步状态不一定是“有没有锁”AQS 的核心状态是一个 volatile int通过 getState()、setState() 和 compareAndSetState() 访问或更新。假设ReentrantLock的状态变化如下初始 state 0T1 第一次 lock state 1owner T1T1 再次 lock state 2owner T1T1 第一次 unlock state 1owner T1T1 第二次 unlock state 0owner null因此重入锁中的state 2表示同一个线程持有两次不是两个线程同时持有锁。2、volatile 不能代替 CAS设想 T1 和 T2 同时执行下面的错误逻辑// 错误示意检查和修改是两个独立步骤。 if (getState() 0) { setState(1); return true; }两个线程可能都读到 0然后都认为自己成功了。volatile可以提供可见性和相应的内存顺序但不能让这组操作成为一个整体。竞争首次获取时需要原子地执行“如果仍然是 0就改成 1”。CAS 让同一次从 0 到 1 的竞争只有一个赢家。不过并不是每次修改都必须 CAS。对于已经独占持锁的线程修改自己的重入次数时没有其他线程可以合法地并发修改这个计数因而可以直接更新状态。3、owner 用来识别重入和非法释放state 只能说明持有次数无法回答“谁持有”。ReentrantLock 还利用父类 AbstractOwnableSynchronizer 提供的持有者字段记录线程。于是获取逻辑分为三种情况state 0尝试原子占有并记录持有者state 0 且持有者是当前线程增加重入次数state 0 且持有者是其他线程获取失败释放时则检查当前线程是不是持有者再扣减计数。只有计数归零才清除持有者并允许其他线程获取。四、同步队列获取锁失败的线程去哪里AQS 的同步队列借鉴了 CLH 队列锁的思想但它增加了显式前后链接、阻塞唤醒和取消处理不能直接等同于传统的 CLH 自旋锁。在稳定状态下队列可以描述为如下时间紧张不再画具体的图例了head tail│ │▼ ▼[头节点] —— [T2 等待节点] —— [T3 等待节点] —— [T4 等待节点]队列按需初始化初始头节点是哨兵。已入队线程获取锁成功后其节点会成为新的头节点等待线程引用会被清理。多个线程可能同时追加节点所以更新尾指针需要 CAS。尾指针更新成功与前驱的next链接补全并不是同一个原子步骤。比如源码private Node addWaiter(Node mode) { Node node new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred tail; if (pred ! null) { node.prev pred; if (compareAndSetTail(pred, node)) { pred.next node; return node; } } enq(node); return node; }五、一次竞争加锁到底经历了什么假设 T1 已经持锁T2 和 T3 随后调用lock()1、先尝试再进入队列T2 到达时先尝试获取。只有快速获取和后续重试都没有成功才需要排队。调用链如下ReentrantLock.lock()└─ Sync.lock()├─ NonfairSync.initialTryLock()快速获取或重入└─ AQS.acquire(1)前一步失败才执行├─ NonfairSync.tryAcquire(1)再次尝试└─ AQS.acquire(node, arg, ...)进入获取循环1ReentrantLock.lock()把调用转交给sync.lock()。Sync中的入口很短final void lock() { if (!initialTryLock()) acquire(1); }2关键在于第一步的动态分派。默认使用NonfairSync所以执行下面的快速路径final boolean initialTryLock() { Thread current Thread.currentThread(); if (compareAndSetState(0, 1)) { // first attempt is unguarded setExclusiveOwnerThread(current); return true; } else if (getExclusiveOwnerThread() current) { int c getState() 1; if (c 0) // overflow throw new Error(Maximum lock count exceeded); setState(c); return true; } else return false; }按执行顺序看有三个分支首次占有成功。直接尝试把state从 0 改为 1成功后记录持有者并返回。这里没有检查是否存在排队线程是非公平获取允许插队的一个入口。当前线程重入。CAS 失败后检查持有者是不是自己。如果是只增加持有次数无须入队。溢出检查防止计数越界。其他情况失败。例如锁仍由其他线程持有返回false交给 AQS 继续处理。3快速路径失败后AQS 还会再试一次。代码如下public final void acquire(int arg) { if (!tryAcquire(arg)) acquire(null, arg, false, false, false, 0L); } protected final boolean tryAcquire(int acquires) { if (getState() 0 compareAndSetState(0, acquires)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; }前面的快速尝试失败不代表此刻仍然无法获取持锁线程可能刚刚释放。因此进入队列机制之前再试一次可能省下后续的节点分配和线程阻塞。acquire参数依次表示还没有等待节点、获取参数、独占模式、不中断退出、不使用超时以及此路径不使用的截止时间。这些参数让独占、共享、可中断和超时获取复用内部循环。acquire(null, arg, false, false, false, 0L);4对于还没有入队的线程内部循环仍允许再次尝试获取。若还未成功才执行节点创建和入队分支。特别注意第一次走到这里可能只是创建节点。下一轮还会重新检查能否获取并不一定立即入队。真正追加节点时先记录观察到的尾节点再通过casTail(t, node)竞争尾部位置队列未初始化就先尝试建立哨兵头节点之后继续循环。CAS 失败说明尾部已经变化本次追加没有成功清除临时前驱引用之后重试。CAS 成功再补上旧尾节点的next链接。// 内部 acquire 的局部方法 if (node null) { if (shared) node new SharedNode(); else node new ExclusiveNode(); } else if (pred null) { node.waiter current; Node t tail; node.setPrevRelaxed(t); if (t null) tryInitializeHead(); else if (!casTail(t, node)) node.setPrevRelaxed(null); else t.next node; }compareAndSetState竞争锁casTail竞争队列位置。成功入队只表示开始排队不能进入临界区。回到 T2 的例子如果 T1 一直没有释放T2 会完成入队T3 随后追加在它后面。接下来需要决定什么时候可以安全休眠。2、准备休眠之前要通知其他等待者一个不靠谱的时序是T2发现锁不可用T1释放锁T2开始休眠如果 T1 没有发现需要通知的等待者而 T2 又没有重新检查T2 就可能在锁已经空闲时一直睡下去。JDK 17 的处理方式是等待节点设置WAITING状态后回到循环重新检查获取条件真正阻塞前也会检查节点状态。释放方则在通知时原子清除后继节点的WAITING位并调用 LockSupport.unpark。else if (node.status 0) { node.status WAITING; // enable signal and recheck } else { long nanos; spins postSpins (byte)((postSpins 1) | 1); if (!timed) LockSupport.park(this); else if ((nanos time - System.nanoTime()) 0L) LockSupport.parkNanos(this, nanos); else break; node.clearStatus(); if ((interrupted | Thread.interrupted()) interruptible) break; }3、park 返回之后还得重新竞争唤醒意味着线程有机会继续执行并不代表锁已经归它所有。例如在非公平锁下T1释放锁通知 T2T4新到达抢先把 state 从 0 改成 1T2恢复执行发现锁又被占用T2继续等待所以 AQS 需要一个获取循环。只有获取规则确认成功线程才能从加锁方法返回。其中的核心判断逻辑如下if (first || pred null) { boolean acquired; try { if (shared) acquired (tryAcquireShared(arg) 0); else acquired tryAcquire(arg); } catch (Throwable ex) { cancelAcquire(node, interrupted, false); throw ex; } // 后续处理 acquired成功则返回失败则继续循环。 }这两个条件各有含义条件当前处境为什么允许尝试pred null普通获取路径中尚未成功入队锁可能已释放先试有机会避免排队first当前节点的前驱是head当前线程是队首等待者有资格推进队列后面的等待节点不会在每轮循环里都去争抢state。如果发现前驱已经取消源码会调用cleanQueue()如果前驱正处于成为头节点的过渡阶段则会短暂自旋等待结构稳定。对已经排队的 T2 而言只有成为队首等待者才会进入这里的tryAcquire(arg)。如果仍然失败就继续走前面介绍的等待分支如果成功才进入最后一步。4、成功后推进 head假设 T2 最终成功它原来的等待节点成为新头节点T3 成为队首等待者。这时源码更新队列结束等待if (acquired) { if (first) { node.prev null; head node; pred.next null; node.waiter null; if (shared) signalNextIfShared(node); if (interrupted) current.interrupt(); } return 1; }对于已经排队的独占获取执行顺序是tryAcquire 成功当前线程已经获得锁↓自己的等待节点成为 head清理旧链接和 waiter↓必要时恢复中断标志结束获取流程对应的队列变化如下图中的 T2 节点在成为头节点后已清除waiter这里只标出它的来源获取前head → [旧头节点] ↔ [T2 等待节点] ↔ [T3 等待节点]获取后 head → [ T2 节点] ↔ [T3 等待节点]T2 从lock()返回并进入临界区T3 成为队首等待者。等到 T2 完全释放T3 才有机会沿同样的过程继续获取。六、释放锁先改变资源状态再通知等待者释放逻辑最容易误解的地方是把“调用了一次 unlock”当成“锁已经空闲”。对于重入锁假设当前计数为 2第一次 unlock2 → 1仍然持锁第二次 unlock1 → 0完全释放只有完全释放时独占释放钩子才返回trueAQS 才进入相应通知流程。在无取消干扰的正常路径下释放方通知队首等待者而不是一次唤醒整个队列。否则大量线程被唤醒后仍然只能有一个成功其他线程又要阻塞徒增调度和竞争开销。七、LockSupport 为什么允许“先通知后休眠”LockSupport为每个线程提供一个最多为 1 的许可可以用下面的模型理解操作含义unpark(thread)使目标线程的许可可用必要时解除阻塞park()有许可则消耗后返回否则可能阻塞连续多次unpark不会积累出多个许可如果unpark先发生且许可没有被其他park消耗后续park可以直接返回。这解决了通知先于实际阻塞发生的一类竞态。八、总结理解AQS最有用的一条主线是state 决定能否通过队列组织等待park/unpark 协调休眠与恢复线程醒来之后仍然必须重新检查获取条件。沿着这条主线重入、公平性、共享传播以及 Condition 的队列转移就能连成一个完整过程。