1. 为什么需要深入理解AQS?
在Java并发编程的世界里,AbstractQueuedSynchronizer(AQS)就像是一座隐藏在水面下的冰山。你可能每天都在使用基于它构建的工具(如ReentrantLock、CountDownLatch),却未必真正了解它的运作机制。我第一次面试高级Java开发岗位时,面试官让我在白板上画出AQS的等待队列结构,那一刻我才意识到,仅仅会使用同步器是远远不够的。
AQS是Java并发包(JUC)中最核心的基础设施,它采用模板方法模式,为各种同步器提供了底层的排队、阻塞和唤醒机制。理解AQS的工作原理,不仅能让你在面试中游刃有余(特别是面对"请手写一个自定义锁"这类问题时),更重要的是,当你在生产环境遇到死锁、线程饥饿等复杂问题时,能够快速定位到根本原因。
2. AQS的核心设计哲学
2.1 同步状态的管理艺术
AQS使用一个volatile修饰的int类型变量(state)来表示同步状态,这个简单的设计蕴含着深刻的并发智慧。以ReentrantLock为例,state=0表示锁未被占用,state=1表示被占用,state>1表示重入次数。这种设计既保证了内存可见性,又通过CAS操作实现了无锁化的状态变更。
java复制// AQS中状态变量的定义
private volatile int state;
// 典型的CAS操作示例
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
关键点:volatile保证可见性,CAS保证原子性,这种组合是并发编程的经典模式。但在高竞争场景下,CAS的忙等可能导致CPU飙升,这时就需要用到AQS的队列机制。
2.2 CLH队列的变体实现
AQS的等待队列是CLH锁的变体,但做了重要改进:
- 每个节点保存前驱节点的引用(用于唤醒检查)
- 每个节点通过自旋监测前驱状态(而非不断轮询锁状态)
- 引入"取消"状态快速清理无效节点
java复制static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter; // 用于条件队列
}
我在一次性能调优中发现,当线程竞争激烈时,队列节点的创建和销毁会成为瓶颈。这时可以考虑使用ThreadLocal缓存节点对象,但要注意内存泄漏问题。
3. 关键方法源码逐行解析
3.1 acquire():获取资源的完整流程
这是AQS最核心的方法之一,我们以独占模式为例:
java复制public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- tryAcquire():由子类实现,尝试直接获取资源
- addWaiter():创建节点并入队
- acquireQueued():在队列中自旋/阻塞等待
- selfInterrupt():恢复中断状态
踩坑记录:我曾经在自定义同步器时忘记处理中断状态,导致上层应用无法正确响应中断。正确的做法是在acquireQueued()返回true时(表示被中断过),调用selfInterrupt()重新设置中断标志。
3.2 addWaiter()的细节魔鬼
java复制private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
这里有个精妙的设计:先尝试快速入队(当队列非空时),失败后再进入完整的enq()流程。但要注意,设置prev和next不是原子操作,这可能导致短暂的链表不一致。
3.3 shouldParkAfterFailedAcquire():等待状态的艺术
java复制private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这个方法处理三种情况:
- 前驱已经是SIGNAL状态(可以安全park)
- 前驱已被取消(跳过这些节点)
- 前驱状态正常(尝试设置为SIGNAL)
4. 从AQS到常用同步器
4.1 ReentrantLock的非公平实现
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;
}
非公平锁的精髓在于:不管有没有线程在排队,新来的线程直接尝试获取锁。这虽然可能导致饥饿,但减少了线程切换的开销。
4.2 CountDownLatch的共享模式
java复制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;
}
}
CountDownLatch使用state初始化为计数器值,每次countDown()递减,await()在state=0时通过。注意releaseShared()必须用循环CAS,因为可能有多个线程同时调用countDown()。
5. 生产环境中的AQS实战技巧
5.1 自定义同步器的正确姿势
假设我们要实现一个简单的TwinsLock(同时允许两个线程获取锁):
java复制class TwinsLock implements Lock {
private final Sync sync = new Sync(2);
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count);
}
protected int tryAcquireShared(int reduceCount) {
for (;;) {
int current = getState();
int newCount = current - reduceCount;
if (newCount < 0 || compareAndSetState(current, newCount)) {
return newCount;
}
}
}
protected boolean tryReleaseShared(int returnCount) {
for (;;) {
int current = getState();
int newCount = current + returnCount;
if (compareAndSetState(current, newCount)) {
return true;
}
}
}
}
// 其他Lock接口方法实现...
}
经验之谈:自定义同步器时,一定要考虑重入性和中断响应。我曾经遇到过因为忽略中断导致线程无法退出的严重问题。
5.2 性能调优的关键指标
- 队列长度监控:通过反射获取head和tail可以估算队列长度
- 自旋次数统计:在acquireQueued()中添加计数器
- CAS失败率:高失败率可能预示竞争激烈
java复制// 示例:获取AQS队列长度(仅供诊断用)
int getQueueLength(Sync sync) throws Exception {
Field tailField = AbstractQueuedSynchronizer.class.getDeclaredField("tail");
tailField.setAccessible(true);
Node tail = (Node) tailField.get(sync);
int count = 0;
while (tail != null) {
count++;
tail = tail.prev;
}
return count;
}
6. AQS的局限与替代方案
虽然AQS非常强大,但在某些场景下并非最佳选择:
- 高并发计数器:LongAdder性能更好
- 短时任务调度:Phaser可能更合适
- 分布式环境:需要基于Redis/ZooKeeper实现
我在一个高频交易系统中就遇到过AQS队列竞争成为瓶颈的情况,最终通过结合ThreadLocal和自旋锁优化,将吞吐量提升了3倍。
