深入解析 AQS:从源码到实践,剖析 ReentrantLock 和 Semaphore 的实现
引言
在 Java 并发编程中,AbstractQueuedSynchronizer
(AQS)是一个核心框架,它为构建锁和其他同步器提供了基础支持。ReentrantLock
和 Semaphore
是 AQS 的两个典型实现,分别用于实现可重入锁和信号量。本文将从底层源码的角度,深入分析 AQS 的核心机制,并结合 ReentrantLock
和 Semaphore
的实际应用场景,探讨其设计思想与实现细节。
一、AQS 的核心机制
1.1 AQS 的设计思想
AQS 的核心思想是通过一个 FIFO 队列(CLH 队列)来管理线程的排队和唤醒机制,同时结合一个 state
变量来表示同步状态。AQS 提供了两种模式:
- 独占模式:同一时刻只有一个线程可以获取资源(如
ReentrantLock
)。 - 共享模式:多个线程可以同时获取资源(如
Semaphore
)。
1.2 核心数据结构
1.2.1 state
变量
state
是 AQS 的核心变量,用于表示同步状态。例如:
- 在
ReentrantLock
中,state
表示锁的重入次数。 - 在
Semaphore
中,state
表示剩余的许可数。
private volatile int state; // 同步状态
1.2.2 CLH 队列
AQS 使用 CLH 队列(Craig, Landin, and Hagersten 锁队列)来管理等待线程。每个线程被封装为一个 Node
节点,节点中保存了线程的状态(如等待、取消)以及前驱和后继节点的引用。
static final class Node {
volatile int waitStatus; // 线程状态
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 等待线程
}
1.3 关键方法
1.3.1 acquire(int arg)
acquire
是获取资源的核心方法。它的主要逻辑如下:
- 调用
tryAcquire
尝试获取资源。 - 如果获取失败,将当前线程封装为
Node
并加入 CLH 队列。 - 通过自旋和
LockSupport.park()
挂起线程,等待唤醒。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1.3.2 release(int arg)
release
是释放资源的核心方法。它的主要逻辑如下:
- 调用
tryRelease
尝试释放资源。 - 如果释放成功,唤醒 CLH 队列中的下一个线程。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
二、ReentrantLock 的实现
2.1 公平锁与非公平锁
ReentrantLock
支持公平锁和非公平锁两种模式:
- 公平锁:严格按照 CLH 队列的顺序获取锁。
- 非公平锁:允许插队,新线程可以直接尝试获取锁。
2.1.1 公平锁的实现
公平锁在 tryAcquire
中会检查是否有前驱节点,如果有则放弃获取锁。
protected final boolean tryAcquire(int acquires) {
if (getState() == 0 && !hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(currentThread());
return true;
}
return false;
}
2.1.2 非公平锁的实现
非公平锁在 tryAcquire
中不会检查前驱节点,直接尝试获取锁。
final boolean nonfairTryAcquire(int acquires) {
if (getState() == 0 && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(currentThread());
return true;
}
return false;
}
2.2 tryLock()
和 lock()
方法
lock()
:调用acquire(1)
,如果获取失败则进入等待队列。tryLock()
:调用tryAcquire(1)
,立即返回是否获取成功。
三、Semaphore 的实现
3.1 信号量的许可数管理
Semaphore
通过 state
变量表示剩余的许可数。acquire()
和 release()
方法分别用于获取和释放许可。
3.1.1 acquire()
acquire
方法会减少 state
的值,如果许可不足则进入等待队列。
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
3.1.2 release()
release
方法会增加 state
的值,并唤醒等待线程。
public void release() {
sync.releaseShared(1);
}
四、实际应用场景
4.1 ReentrantLock 的应用
- 高并发环境下的资源竞争控制:如数据库连接池的并发访问。
- 可重入特性:支持同一线程多次获取锁,避免死锁。
4.2 Semaphore 的应用
- 线程池的任务调度:通过信号量限制并发任务数。
- 限流:控制系统的并发请求数,防止资源耗尽。
五、性能优化和注意事项
5.1 性能优化
- 自旋锁:在短时间内通过自旋尝试获取锁,减少线程切换的开销。
- CAS 操作:通过
compareAndSetState
实现无锁化的状态更新。
5.2 注意事项
- 避免死锁:确保锁的获取和释放成对出现。
- 合理设置超时:使用
tryLock(long timeout, TimeUnit unit)
避免长时间等待。
六、总结与展望
AQS 是 Java 并发包的基石,其设计思想(CLH 队列 + state
变量)为构建高效、灵活的同步器提供了强大支持。ReentrantLock
和 Semaphore
是 AQS 的典型应用,分别解决了独占资源和共享资源的同步问题。
未来,AQS 可能会在以下方面进一步优化:
- 更高效的自旋策略。
- 对 NUMA 架构的更好支持。
- 更灵活的同步模式扩展。
附录
图表:CLH 队列结构
Head -> Node1 -> Node2 -> Node3 -> Tail
关键代码片段
- AQS 的
acquire
和release
方法。 ReentrantLock
的公平锁与非公平锁实现。Semaphore
的acquire
和release
方法。
通过本文的分析,相信读者能够深入理解 AQS 的设计思想及其在 ReentrantLock
和 Semaphore
中的应用,为高并发编程打下坚实基础。