AQS 同步器——Java 并发框架的核心底座全解析
在 Java 并发包 java.util.concurrent (JUC) 中,ReentrantLock、Semaphore、CountDownLatch 等工具几乎统治了多线程同步的半壁江山。而这些强大工具的背后,都站着同一个“幕后英雄”:AQS (AbstractQueuedSynchronizer)。
理解 AQS,就等于拿到了开启 Java 高级并发编程大门的钥匙。今天,我们就剥开这一层层复杂的包装,一步步讲透它的核心原理。
1. 这篇文章要解决什么问题?
如果你要设计一个高性能的锁,你需要考虑哪些问题?
- 状态管理:如何记录锁是被占用了,还是空闲着?
- 原子竞争:多个线程同时抢锁,如何确保只有一个能成功?
- 线程排队:没抢到锁的线程,该安置在哪里?如何让他们有序地等待?
- 唤醒机制:持锁线程释放后,该通知哪一个等待线程来接手?
AQS 的出现,就是为了将这些“脏活累活”统一封装起来。它提供了一套通用的同步管理框架,让开发者只需关注业务本身的“获取/释放”逻辑,而无需操心复杂的队列管理和线程调度。
2. 核心原理:AQS 的三大基石
AQS 能够高效运转,全靠这三个核心组件:
核心一:volatile state (同步状态位)
AQS 内部维护了一个名为 state 的整型变量。
- 语义:通常 0 代表资源空闲,>=1 代表资源被占用。对于可重入锁,
state的数值代表线程重入的次数。 - 可见性:通过
volatile修饰,确保多线程下状态的实时可见。
核心二:CAS (原子的变更)
当多个线程尝试修改 state 时,AQS 使用 Unsafe 类提供的 CAS (Compare-And-Swap) 操作。只有 CAS 成功,才代表线程成功抢占到了资源。这是一种无锁的乐观策略,极大减少了传统重量级锁的开销。
核心三:CLH 变体队列 (虚拟双向队列)
这是 AQS 最复杂的部分。它将没抢到锁的线程封装成一个 Node 节点,并放入一个双向链表中。
- 虚拟性:队列中并不真的存储线程对象,而是通过 Node 中的指针维持一个“等待链”。
- 头结点 (Head):始终代表当前持有锁的线程。
3. 流程/机制描述:独占锁的获取全流程
我们以 acquire(int arg) 方法为例,看看 AQS 是如何一步步处理抢锁逻辑的。
第一步:尝试获取 (TryAcquire)
AQS 使用 模板方法模式。它并不实现具体的获取逻辑,而是调用子类实现的 tryAcquire。子类通常在这里判断 state == 0 并尝试 CAS 变更。
第二步:入队 (AddWaiter)
如果 tryAcquire 失败,当前线程会被封装成一个 Node(共享或独占模式),并通过 CAS 挂载到等待队列的尾部(Tail)。
第三步:自旋与阻塞 (AcquireQueued)
入队后的线程并不会立即睡觉,它会检查:“我的前驱节点是否是 Head?”
- 如果是,说明我排第二,有资格再次尝试抢锁(因为 Head 可能随时释放)。
- 如果抢到了,我就变成新的 Head。
- 如果抢不到,或者前驱不是 Head,线程会通过
LockSupport.park()挂起,进入阻塞状态,等待被唤醒。
4. 常见误区
误区 1:AQS 队列里存的是线程本身
辨析:队列存的是 Node 对象,它包含指向线程的引用、等待状态 waitStatus 以及前后驱指针。这使得 AQS 可以更灵活地管理取消、超时等复杂场景。
误区 2:只要用了 AQS 性能就一定高
辨析:AQS 的性能很大程度上取决于子类 tryAcquire 是否高效,以及在高竞争下的唤醒/挂起频率。此外,独占模式下的“自旋”次数也是影响性能的关键因素。
5. 实际工作中怎么用?
- 理解组件基座:当你查看
ReentrantLock源码时,你会发现它内部只有两个子类FairSync和NonfairSync继承了 AQS。掌握了 AQS,你就秒懂了公平锁与非公平锁的差异仅仅在于tryAcquire的入队检查。 - 复杂同步场景模拟:某些极端场景下(如限制只有 3 个线程能同时写 A 业务,1 个能读 B 业务),现有的并发工具可能不够灵活,你可以尝试基于 AQS 定制同步器。
- 监控与诊断:在排查多线程死锁或性能瓶颈时,通过检查 AQS 同步器的
getQueueLength()或hasQueuedThreads(),可以清晰判断压力是否堆积在排队环节。
总结
AQS 是面向对象设计中“解耦”和“复用”的巅峰之作。它将复杂的线程调度抽象为简单的状态管理,为 Java 构建了极其稳固的并发地基。作为中高级开发者,唯有透彻理解这个“底座”,才能在并发编程的海洋中游刃有余。