1. 从锁的演进看AQS的价值
在Java并发编程的发展历程中,锁机制经历了从重量级到轻量级的演变。早期的synchronized关键字虽然简单易用,但其底层实现依赖于操作系统的互斥量(Mutex),每次获取和释放锁都需要从用户态切换到内核态,这种上下文切换的开销在高并发场景下变得难以接受。
2004年JDK 1.5发布时,Doug Lea大师在J.U.C包中引入了AbstractQueuedSynchronizer(AQS)框架,彻底改变了Java锁的实现方式。AQS通过以下几个核心设计解决了性能问题:
- CLH队列变种:采用FIFO双向链表实现线程排队,避免了操作系统级别的阻塞
- 状态位原子操作:通过volatile变量和CAS操作实现轻量级同步
- 模板方法模式:将锁的获取/释放逻辑抽象为可重写的方法
关键理解:AQS不是完整的锁实现,而是提供了构建锁和同步器的框架。正如其名"抽象队列同步器",它抽象了同步状态的管理和线程排队机制,具体获取/释放资源的逻辑由子类实现。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ReentrantLock与AQS的共生关系
2.1 类结构剖析
打开ReentrantLock源码,其核心实现依赖于内部类Sync,而Sync正是继承自AQS:
java复制public class ReentrantLock implements Lock {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
// 实现AQS的tryAcquire等方法
}
static final class NonfairSync extends Sync {...}
static final class FairSync extends Sync {...}
}
这种设计体现了典型的"组合优于继承"原则。ReentrantLock将锁操作委托给Sync实例,而Sync根据构造参数决定使用公平锁(FairSync)还是非公平锁(NonfairSync)。
2.2 同步状态的精妙设计
AQS使用一个volatile int类型的state变量来表示同步状态。在ReentrantLock中:
- state=0:锁未被任何线程持有
- state=1:锁被某个线程独占
- state>1:锁被重入,数值表示重入次数
这种设计使得可重入特性变得异常简洁。每次重入只需state+1,释放时state-1,直到state=0才真正释放锁。
3. 公平锁与非公平锁的实现差异
3.1 非公平锁的抢占逻辑
NonfairSync的tryAcquire实现体现了"插队"特性:
java复制final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { // 直接尝试CAS获取
setExclusiveOwnerThread(current);
return true;
}
}
// 重入逻辑...
}
关键点在于:即使队列中有等待线程,新来的线程仍可以直接尝试获取锁。这种设计减少了线程切换,提高了吞吐量,但可能导致"线程饥饿"。
3.2 公平锁的严格排队
FairSync的实现则严格遵守FIFO原则:
java复制protected final boolean tryAcquire(int acquires) {
if (getState() == 0 &&
!hasQueuedPredecessors() && // 检查是否有前驱节点
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
// 重入逻辑...
}
hasQueuedPredecessors()方法会检查当前线程是否是CLH队列的头节点,如果不是则获取失败。这种严格的顺序保证了公平性,但增加了上下文切换开销。
4. 锁获取的完整流程解析
4.1 acquire的标准化流程
无论是公平锁还是非公平锁,最终都会调用AQS的acquire方法:
java复制public final void acquire(int arg) {
if (!tryAcquire(arg) && // 子类实现的获取逻辑
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 加入队列并等待
selfInterrupt();
}
这个模板方法定义了锁获取的标准流程:
- 先尝试快速获取(tryAcquire)
- 失败后创建节点加入队列(addWaiter)
- 在队列中等待(acquireQueued)
4.2 CLH队列的运作机制
AQS的等待队列采用CLH锁的变体实现,每个节点保存着线程引用和等待状态:
java复制static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}
队列操作的精妙之处在于:
- 入队操作总是通过CAS更新tail指针
- 出队时只需将head指向下一个节点
- 前驱节点的状态变化会通知后继节点
5. 条件变量的实现原理
5.1 ConditionObject的内部结构
ReentrantLock的条件等待功能通过内部类ConditionObject实现:
java复制public class ConditionObject implements Condition {
private transient Node firstWaiter;
private transient Node lastWaiter;
public final void await() throws InterruptedException {
Node node = addConditionWaiter(); // 加入条件队列
int savedState = fullyRelease(node); // 完全释放锁
// ...
}
}
关键点在于:
- 每个ConditionObject维护独立的条件队列
- await()会释放锁并进入条件队列
- signal()将节点从条件队列转移到同步队列
5.2 await/signal的协作流程
一个完整的等待-通知流程如下:
- 线程A调用await():
- 加入条件队列
- 完全释放锁
- 进入等待状态
- 线程B调用signal():
- 将线程A的节点转移到同步队列
- 线程A在同步队列中尝试重新获取锁
- 线程A获取锁后从await()返回
6. 性能优化与实战技巧
6.1 锁选型建议
根据实际场景选择锁策略:
- 高吞吐场景:非公平锁(默认),减少线程切换
- 公平性要求高:公平锁,避免线程饥饿
- 读多写少:考虑ReadWriteLock
- 短暂等待:尝试tryLock带超时参数
6.2 调试与监控技巧
-
查看锁状态:
java复制ReentrantLock lock = new ReentrantLock(); System.out.println("锁是否被持有: " + lock.isLocked()); System.out.println("持有锁的线程: " + lock.getOwner()); System.out.println("等待队列长度: " + lock.getQueueLength()); -
死锁诊断:
- 使用jstack查看线程堆栈
- 关注"Locked ownable synchronizers"部分
- 查找互相等待的锁资源
-
性能监控:
java复制ReentrantLock lock = new ReentrantLock(); System.out.println("是否是公平锁: " + lock.isFair()); System.out.println("重入次数: " + lock.getHoldCount());
7. 常见问题排查指南
7.1 锁未释放导致的内存泄漏
典型症状:
- 线程池中的任务卡死
- 应用响应变慢最终无响应
排查步骤:
- 使用jstack获取线程dump
- 查找处于WAITING状态的线程
- 检查其堆栈中的锁获取位置
- 确认是否在finally块中释放锁
7.2 重入锁使用不当
易错场景:
java复制lock.lock();
try {
if (condition) {
lock.lock(); // 重复获取
try {
// ...
} finally {
lock.unlock();
}
}
} finally {
lock.unlock(); // 可能提前释放
}
正确做法:
- 保持lock/unlock严格配对
- 使用getHoldCount()检查重入次数
- 考虑使用带条件的锁获取
8. AQS的扩展应用
虽然本文聚焦ReentrantLock,但AQS的应用远不止于此:
-
共享锁:如Semaphore、CountDownLatch
- state表示可用许可数
- tryAcquireShared/tryReleaseShared实现共享模式
-
读写锁:ReentrantReadWriteLock
- 高16位表示读锁状态
- 低16位表示写锁状态
- 实现读共享、写独占
-
自定义同步器:如实现一个简单的门闩:
java复制class SimpleLatch extends AbstractQueuedSynchronizer { protected int tryAcquireShared(int acquires) { return (getState() == 1) ? 1 : -1; } protected boolean tryReleaseShared(int releases) { setState(1); return true; } }
在实际项目中,理解AQS的设计思想比记住API更重要。当需要实现特定同步机制时,考虑是否可以基于AQS构建,往往能事半功倍。
