1. AQS是什么?为什么说它是Java并发基石
我第一次接触AQS是在2015年,当时在解决一个高并发订单系统的锁竞争问题时,发现直接用synchronized性能完全扛不住。在翻阅JUC源码时,这个神秘的AbstractQueuedSynchronizer类引起了我的注意。经过几周的源码研读和实际验证,我才真正理解为什么Doug Lea会把它称为"Java并发框架的基础设施"。
AQS本质上是一个构建锁和同步器的框架,它用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。你可能不知道的是,Java中那些耳熟能详的并发工具,比如ReentrantLock、Semaphore、CountDownLatch等,它们的核心实现都是基于AQS完成的。这种设计模式非常巧妙——将同步器的公共行为抽象到AQS中,而具体的同步器只需要实现几个关键方法就能完成定制。
提示:AQS采用了模板方法设计模式,定义了获取/释放同步状态的主流程,而将具体的状态操作留给子类实现
2. AQS的核心实现原理
2.1 同步状态与CLH队列
AQS最核心的两个概念就是同步状态(state)和CLH队列。state是一个volatile int变量,不同的同步器对它的解释不同。比如在ReentrantLock中,state表示持有锁的线程获取锁的次数;在Semaphore中,state表示剩余的许可数量;在CountDownLatch中,state则表示计数器的当前值。
CLH队列是Craig、Landin和Hagersten三位大牛名字的缩写,它是一种基于链表的可扩展、高性能、公平的自旋锁实现。AQS中的等待队列就是CLH队列的一个变种。每个等待线程都会被封装成一个Node节点,这个节点保存了线程引用、等待状态以及前后节点的引用。
java复制static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
// ...
}
2.2 独占模式与共享模式
AQS支持两种同步模式:独占模式(Exclusive)和共享模式(Share)。这两种模式对应着不同的资源获取方式:
- 独占模式:同一时刻只有一个线程能获取资源,如ReentrantLock
- 共享模式:多个线程可以同时获取资源,如Semaphore、CountDownLatch
这两种模式的核心区别体现在获取和释放资源的方式上。在独占模式下,acquire()和release()方法会完全独占资源;而在共享模式下,acquireShared()和releaseShared()方法允许多个线程共同访问资源。
2.3 模板方法设计
AQS采用了模板方法模式,定义了获取和释放资源的骨架流程,而将具体的状态操作留给子类实现。子类需要重写以下几个关键方法:
java复制// 独占模式下获取同步状态
protected boolean tryAcquire(int arg)
// 独占模式下释放同步状态
protected boolean tryRelease(int arg)
// 共享模式下获取同步状态
protected int tryAcquireShared(int arg)
// 共享模式下释放同步状态
protected boolean tryReleaseShared(int arg)
// 当前同步器是否被当前线程独占
protected boolean isHeldExclusively()
这种设计使得实现一个自定义同步器变得非常简单。比如要实现一个不可重入的互斥锁,只需要这样:
java复制class Mutex {
private static class Sync extends AbstractQueuedSynchronizer {
protected boolean tryAcquire(int ignore) {
return compareAndSetState(0, 1);
}
protected boolean tryRelease(int ignore) {
setState(0);
return true;
}
}
private final Sync sync = new Sync();
public void lock() { sync.acquire(0); }
public void unlock() { sync.release(0); }
}
3. AQS在JUC中的典型应用
3.1 ReentrantLock的实现
ReentrantLock是AQS最经典的实现之一。它的内部类Sync继承自AQS,并通过FairSync和NonfairSync两个子类分别实现了公平锁和非公平锁。
非公平锁的tryAcquire实现:
java复制final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
这里有几个关键点:
- 首先尝试CAS设置state,成功则获取锁
- 如果state不为0,检查是否是当前线程持有锁(可重入)
- 可重入时增加state计数
- 非公平体现在不检查队列直接尝试获取锁
3.2 Semaphore的工作原理
Semaphore(信号量)是AQS共享模式的典型应用。它通过控制一定数量的"许可"来限制线程访问特定资源的数量。
java复制// 获取许可
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 释放许可
public void release() {
sync.releaseShared(1);
}
Semaphore的tryAcquireShared实现:
java复制protected int tryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
这个实现展示了经典的"循环+CAS"模式,确保在多线程环境下正确计算剩余许可数。
3.3 CountDownLatch的机制
CountDownLatch是另一个基于AQS的实用工具,它允许一个或多个线程等待其他线程完成操作。
java复制public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void countDown() {
sync.releaseShared(1);
}
CountDownLatch的tryAcquireShared实现非常简洁:
java复制protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
当state减到0时,所有等待线程都会被唤醒。这种设计非常适合主线程等待多个工作线程完成的场景。
4. AQS的高级特性与实战技巧
4.1 条件变量的实现
AQS中的ConditionObject实现了条件变量功能,它与内置的监视器条件队列类似,但提供了更灵活的功能。每个ConditionObject都维护一个独立的条件队列。
java复制final ConditionObject newCondition() {
return new ConditionObject();
}
使用示例:
java复制class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
// ... 类似的take方法
}
注意:与Object.wait()/notify()不同,Condition的await()/signal()必须在持有锁的情况下调用
4.2 自定义同步器实战
假设我们要实现一个简单的门闩(Latch),当达到指定数量的线程调用await()时,所有线程才能继续执行:
java复制public class SimpleLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) { setState(count); }
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
private final Sync sync;
public SimpleLatch(int count) {
sync = new Sync(count);
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void countDown() {
sync.releaseShared(1);
}
}
这个实现展示了如何利用AQS快速构建一个实用的同步工具。
4.3 性能优化与注意事项
-
避免过度同步:虽然AQS性能很好,但不必要的同步仍然会带来开销。我曾在一个高并发系统中,通过减少锁粒度(将一个大锁拆分为多个小锁)使吞吐量提升了3倍。
-
注意死锁风险:AQS本身不会导致死锁,但不合理的使用方式会。比如在持有锁A的情况下尝试获取锁B,而另一个线程正持有锁B尝试获取锁A。
-
考虑公平性问题:非公平锁通常有更高的吞吐量,但可能导致线程饥饿。在严格要求公平性的场景,应该使用公平锁。
-
调试技巧:当遇到死锁或性能问题时,可以:
- 使用jstack查看线程状态和锁持有情况
- 使用JMC或VisualVM分析锁竞争情况
- 在测试环境开启-XX:+PrintConcurrentLocks查看锁信息
5. AQS的局限性与替代方案
虽然AQS非常强大,但它并非适用于所有场景:
-
响应式编程场景:在响应式编程中,基于回调的非阻塞模型(如Reactor、RxJava)可能比基于AQS的阻塞模型更合适。
-
超高性能场景:对于极端性能要求的场景,可以考虑:
- 无锁数据结构(如Disruptor)
- 基于CAS的自旋锁
- 线程本地存储减少竞争
-
分布式环境:AQS只适用于单JVM内的同步,分布式锁需要考虑Redis、Zookeeper等方案。
-
协程场景:在Kotlin协程或Project Loom虚拟线程中,传统的锁机制可能不是最佳选择。
我在实际项目中就遇到过这样的案例:一个高频交易系统最初使用了基于AQS的锁,但在极端压力测试下性能仍不达标。后来我们改用Disruptor无锁队列,性能提升了近10倍。这个经验告诉我,技术选型必须结合实际场景。
