⭐ 推荐:社区规则条款 V1.0

Java 中 AQS 原理怎么理解?

lqyyzz
lqyyzz 观望站长
发布于 2026-10-17 21:35 ·5 浏览 ·0 回复
内容摘要

AQS 由 volatile int 同步状态、双向 CLH 等待队列和五个模板方法构成;子类定义 state 含义并实现 tryAcquire 等接口,线程抢锁失败后先入队,再标记前驱,自旋确认后 park 阻塞。

AQS 的原理可以用一句话概括:它是 java.util.concurrent.locks.AbstractQueuedSynchronizer 的缩写,内部只做三件事——维护一个 volatile int state 表示同步状态、维护一个双向 FIFO 等待队列存放没抢到资源的线程、用模板方法把「抢-失败入队-阻塞-唤醒-重试」这套流程固定下来,子类只需要定义 state 的含义和修改规则。JDK 1.5 引入,作者 Doug Lea,ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 全部基于它实现。

AQS 的内部结构由哪几部分组成?

结论:AQS = 一个 int 状态位 + 一个变体 CLH 队列 + 五个模板方法,没有别的魔法。

state 是 volatile int,所有读写通过 getState()、setState()、compareAndSetState() 三个 protected 方法完成,后者底层是 CAS(Compare-And-Swap,比较并交换,CPU 原子指令)。队列是 CLH 锁队列的变体,改成双向链表,节点类型是内部类 Node,字段包括 prev、next、thread 和 waitStatus;waitStatus 有四个取值:CANCELLED=1(已取消)、SIGNAL=-2(后继节点需要被唤醒)、CONDITION=-3(在条件队列中等待)、PROPAGATE=-4(共享模式下唤醒需要继续传播),新建节点默认是 0。

需要子类实现的方法是 tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared、isHeldExclusively,AQS 里的默认实现是直接 throw new UnsupportedOperationException(),这也是为什么你不能直接把 AQS 当锁用。

state 在不同同步器里到底代表什么?

结论:state 本身没有语义,含义完全由子类定义,这是 AQS 能一套代码撑起半个 JUC 包的根本原因。

ReentrantLock 里 state 是重入次数,第一次 lock() 从 0 CAS 到 1,同一线程再次进入时判断 getExclusiveOwnerThread() == Thread.currentThread() 后直接 setState(state + 1),所以重入上限是 Integer.MAX_VALUE。Semaphore 里 state 是剩余许可数,acquire(n) 就是把 state 减 n。CountDownLatch 里 state 是初始计数,countDown() 每次减 1,减到 0 时唤醒所有等待线程。ReentrantReadWriteLock 把 32 位 state 拆成两半:高 16 位记录读锁持有数,低 16 位记录写锁重入次数,所以读锁和写锁的最大数量都是 65535。

线程抢不到锁之后发生了什么?

结论:抢不到不会立刻阻塞,而是先入队、再把前驱节点标记为 SIGNAL、然后自旋两次确认拿不到才调用 LockSupport.park() 挂起。

以非公平 ReentrantLock.lock() 为例,调用链是 lock() → acquire(1) → tryAcquire(1) 失败 → addWaiter(Node.EXCLUSIVE) 把当前线程包装成节点追加到队尾 → acquireQueued(node, 1) 进入自旋。自旋逻辑是:先判断前驱是不是 head,是就再试一次 tryAcquire;不是或者又失败,就执行 shouldParkAfterFailedAcquire,把前驱的 waitStatus 从 0 改成 -2(SIGNAL),返回 true 后调用 LockSupport.park(this) 真正挂起。释放时 unlock() → release(1) → tryRelease 成功 → unparkSuccessor(head) 唤醒后继节点。整个过程线程状态在 RUNNABLE 和 WAITING 之间切换,不占用 CPU。

独占模式和共享模式有什么区别?

结论:独占模式同一时刻只允许一个线程持有,共享模式允许多个线程同时持有,差别体现在唤醒时的传播行为上。

ReentrantLock、ReentrantReadWriteLock 的写锁走独占模式,用 acquire/release。Semaphore、CountDownLatch、ReadWriteLock 的读锁走共享模式,用 acquireShared/releaseShared。共享模式的关键是 PROPAGATE 状态:releaseShared 唤醒后继节点后,如果后继也是共享节点,会继续往后唤醒,把一次释放传播给多个等待者,避免只唤醒一个线程导致其余线程反复 park/unpark。

公平锁和非公平锁在 AQS 里的实现差别是什么?

结论:差别只在一个 hasQueuedPredecessors() 判断,非公平锁在入队前允许插队,公平锁必须先看队列里有没有人。

非公平锁的 tryAcquire 直接 CAS 抢 state,抢到就成功,完全不管队列里是否已有线程在等;抢不到才入队。公平锁的 tryAcquire 在 CAS 之前先调用 hasQueuedPredecessors(),只要队列中存在有效前驱节点就直接返回 false,老老实实排队。因为非公平锁少一次队列判空和上下文切换,ReentrantLock 默认构造器用的是非公平模式,吞吐量更高,代价是理论上存在线程饥饿。new ReentrantLock(true) 才是公平锁。

自己写同步器时要注意什么?

结论:只实现 try 系列方法、所有 state 修改必须走 CAS、不要在 tryAcquire 里自己阻塞线程。

tryAcquire 返回 false 的语义是「这次没抢到,请你把我扔进队列」,不是「获取失败」,框架会接管后续的入队、自旋和 park,你在里面调 Thread.sleep() 或者 park() 会破坏整个队列的唤醒链路。tryRelease 要判断当前线程是不是持有者,不是就抛 IllegalMonitorStateException。ConditionObject 是 AQS 的内部类,await() 会把节点从同步队列移到条件队列并完全释放锁,signal() 再把它移回同步队列,这两步都要求线程已持有锁。最后一句实操建议:写业务代码用现成的 ReentrantLock、Semaphore 就够了,只有在自定义语义的资源(比如某种限流器)时才需要继承 AQS。

总结:AQS 把并发同步拆成「状态位 + 等待队列 + 模板方法」三层,state 定义资源、队列管理排队、模板方法固化流程,理解这三点,ReentrantLock、Semaphore、CountDownLatch 的源码基本就是同一套骨架换了个 state 语义。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2250.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~