简介
在Java中,AbstractQueuedSynchronizer(AQS)是Java并发包(java.util.concurrent.locks)中一个用于构建锁和同步器框架的基础类。提供了一种实现阻塞锁和其他同步组件的底层机制。
基本原理概述
它的核心原理包括以下关键点:
- 状态管理:
AQS通过一个volatile类型的整型变量state来表示同步状态。比如在独占锁(如ReentrantLock)中,state为0表示锁未被任何线程持有,大于0则表示当前持有锁的线程数量以及重入次数。 - 等待队列:
AQS维护了一个FIFO双向链表作为同步队列,即CLH队列,用于存放等待获取锁或同步状态的线程。当线程尝试获取锁但发现状态不可用时,会将自己包装成一个节点(Node)并加入到队列尾部进行自旋或挂起等待。 - 获取/释放同步状态:
提供了tryAcquire()和tryRelease()等模板方法给子类去具体实现。这些方法决定了如何基于state值去尝试获取或释放同步状态。例如,在非公平锁中,tryAcquire()可能直接尝试获取锁,而在公平锁中,它会检查是否有其他线程等待更长时间。 - 线程阻塞与唤醒:
利用Unsafe类或者其他并发工具对线程进行阻塞和唤醒操作。当线程无法立即获取锁时,会调用acquireQueued()方法将线程放入等待队列,并进入park()方法挂起;而当锁释放时,则会从等待队列中的某个节点开始唤醒等待线程,使其重新尝试获取锁。 - 可重入性支持:
AQS可以支持重入,这意味着已经获得锁的线程可以再次成功请求该锁,对应的state会递增以记录重入次数。 - 共享模式与独占模式:
AQS同时支持独占模式(只有一个线程能获取到同步状态)和共享模式(多个线程可以同时获取到同步状态),分别对应于ReentrantLock、Semaphore等不同的并发组件。 - 中断处理:
当等待线程被中断时,AQS会根据中断策略进行相应处理,这通常由具体的同步组件决定,可以通过覆盖tryAcquireSharedInterruptibly()等方法实现。
通过继承AQS并实现上述抽象方法,开发者可以创建各种复杂的同步组件,如互斥锁、信号量、读写锁等,无需关注底层的线程调度和阻塞/唤醒逻辑,大大简化了并发编程的复杂度。
核心流程说明
AbstractQueuedSynchronizer(AQS)的流程主要包括线程获取和释放同步状态以及在无法立即获取时如何进入等待队列、唤醒后续线程等步骤。
以下是其核心流程概述:
-
获取同步状态:
- 当线程尝试获取同步状态时,首先调用子类重写的tryAcquire(int arg)方法。
- 如果该方法返回true,表示线程成功获取到同步状态,并执行相应的业务逻辑。
- 如果返回false,表示当前不能获取到同步状态,线程需要被放入同步队列中等待。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
- addWaiter(Node.EXCLUSIVE)将当前线程包装成一个独占模式的Node节点并插入同步队列尾部。
- acquireQueued(Node node, int arg)让线程在队列中自旋或阻塞等待,直到获取到同步状态或者被中断。
- 当线程尝试获取同步状态时,首先调用子类重写的tryAcquire(int arg)方法。
-
等待与自旋:
- 在同步队列中的线程会不断地检查自己的前驱节点是否为头节点,如果是,则再次尝试获取同步状态。
- 若非头节点或尝试获取失败,则通过循环+CAS的方式更新节点的waitStatus值,并可能进入Park操作进行线程挂起。
-
释放同步状态:
- 线程在完成任务后调用release(int arg)来释放同步状态。
- 调用子类重写的tryRelease(int releases)方法,如果该方法返回true,表示同步状态成功释放
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
-
唤醒等待线程:
- unparkSuccessor(Node node)方法从同步队列中找到第一个处于等待状态且合法的节点(通常是头节点的下一个节点),然后调用LockSupport.unpark(s.thread)解除该节点关联线程的阻塞状态,使其有机会再次尝试获取同步状态。
-
可重入性支持:
AQS通过维护一个计数器state来支持锁的可重入性。每当持有锁的线程再次请求时,state递增;当线程退出同步代码块时,state递减,直到state为0时其他线程才能获取到锁。
整个AQS的工作流程围绕着对state变量的操作以及同步队列的管理展开,有效地实现了锁的获取和释放以及线程间的同步协作。
AQS关键源码解析
状态管理(state)
private volatile int state; // 核心状态变量,volatile保证可见性和有序性
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
state字段表示同步状态,通过CAS操作(compareAndSetState方法)来保证原子性的更新。
同步队列(CLH队列)
static final class Node {
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
transient volatile Node head;
transient volatile Node tail;
AQS内部维护了一个FIFO双向链表作为同步队列,节点类型为Node,每个节点代表一个等待获取同步状态的线程。其中head指向队列头节点,tail指向队列尾节点。
获取同步状态(acquire)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
// 子类需要重写tryAcquire方法以实现自定义同步策略
- acquire()方法尝试获取同步状态,首先调用子类实现的tryAcquire()方法尝试获取,若失败,则将当前线程包装成Node并加入到同步队列中,然后在队列中进行自旋或阻塞等待。
- acquireQueued()方法让线程在队列中进行循环等待,直到获取到同步状态或被中断。
AddWaiter方法解析
addWaiter()方法将线程封装为Node并插入队列尾部, 有AQS框架提供。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); // 创建一个新的Node节点,记录当前线程和模式(独占或共享)
// 尝试快速方式插入尾部:首先尝试CAS操作直接设置尾节点的next指向新节点
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) { // 如果CAS成功,则说明已成功更新tail节点
pred.next = node;
return node;
}
}
// 快速插入失败后,需要在enq方法中进行插入操作
enq(node);
return node;
}
// enq方法用于将节点安全地加入到同步队列尾部
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 如果队列为空,则初始化head和tail
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { // 使用CAS将node设为新的tail,并更新旧tail的next指针
t.next = node;
return t;
}
}
}
}
结合上述源码,可以看到基本逻辑如下:
- 创建节点:首先根据传入的模式参数创建一个表示线程等待状态的新节点。
- 快速插入:尝试通过CAS操作将新节点直接插入到现有同步队列的尾部。如果此时有其他线程正在修改队列结构,那么CAS可能失败。
- 循环插入:当CAS操作失败时,调用enq()方法进行循环插入操作,直到成功为止。enq()方法内部是一个无限循环,确保在多线程环境下最终能够将节点安全地插入到队列的尾部。
- 初始化队列:若队列为空,会先初始化队列头节点(同时也是尾节点),然后继续尝试插入新节点。
整个过程保证了节点添加操作的原子性,且能够在高并发情况下正确处理多个线程同时尝试插入节点的情况。
tryAcquire方法解析
tryAcquire是由具体子类实现的,用来定义何时可以获取到锁。
以下是可重入锁ReentrantLock对tryAcquire的实现分析:
tryAcquire(int arg) 方法在 ReentrantLock 中根据公平性和非公平性两种策略有不同的实现。
非公平锁:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 如果锁状态为0,即无锁被持有
if (compareAndSetState(0, acquires)) { // 使用CAS操作尝试设置锁状态
setExclusiveOwnerThread(current); // 设置当前线程为锁的持有者
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 如果当前线程已经持有锁,则支持重入
int nextc = c + acquires;
if (nextc < 0) // 检查是否发生重入计数溢出
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false; // 锁已被其他线程持有,或者CAS操作失败
}
公平锁:
在公平锁中,线程获取锁时需要遵循等待队列的FIFO原则,只有当队列中没有比当前线程更早请求锁的线程时才会尝试获取锁。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 如果锁未被任何线程持有
if (!hasQueuedPredecessors() && // 判断是否有正在等待的线程排在当前线程之前
compareAndSetState(0, acquires)) { // 若没有,则使用CAS尝试获取锁
setExclusiveOwnerThread(current); // 设置当前线程为锁的持有者
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 支持重入
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false; // 锁已被其他线程持有或有等待的线程排在当前线程之前
}
通过以上代码可以看到,tryAcquire() 方法主要包含以下几个步骤:
- 获取当前锁的状态。
- 如果锁空闲(状态为0),则尝试通过CAS操作来获取锁,对于公平锁还需检查同步队列中是否有比当前线程更早的等待线程。
- 如果当前线程已持有锁,执行重入逻辑,增加重入次数。
- 如果锁已经被其他线程持有或无法满足获取条件,则返回false。
释放同步状态(release)
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected boolean tryRelease(int releases) {
throw new UnsupportedOperationException();
}
// 子类需要重写tryRelease方法以实现同步状态的释放逻辑
release()方法尝试释放同步状态,首先调用子类实现的tryRelease()方法释放资源,成功后检查头结点状态,并唤醒其后继节点上的线程。
tryRelease方法解析
tryRelease是由具体子类实现的,用来实现释放锁。
以下是可重入锁ReentrantLock对tryRelease的实现分析:
ReentrantLock 中的 tryRelease(int arg) 方法负责释放锁,其核心功能是更新锁的状态,并在适当情况下唤醒等待队列中的线程。
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 减少当前锁的计数(重入次数)
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 当锁的计数减少到0时
free = true;
setExclusiveOwnerThread(null); // 清除当前独占线程持有者
}
setState(c); // 更新锁的状态
// 唤醒等待队列中的下一个节点,如果有必要的话
if (free) {
unparkSuccessor(node); // node通常是在子类中设置的当前线程对应的Node节点
}
return free;
}
结合上述源码,可以看到大致逻辑如下:
- 减少锁的计数:首先根据传入的参数 releases 减少当前锁的计数,这个计数反映了线程对锁的重入次数。
- 检查线程身份:验证当前执行释放操作的线程是否为锁的所有者,如果不是则抛出异常。
- 判断是否完全释放:当锁的计数减至0时,表示锁被完全释放,此时将独占线程持有者设置为null,并标记变量 free 为true。
- 更新锁状态:无论锁是否完全释放,都需要更新 state 的值。
唤醒等待线程(unparkSuccessor)
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
当同步状态释放时,会调用unparkSuccessor()方法从同步队列中找到第一个等待状态合法的节点,并解除该节点所关联线程的阻塞状态。
总之,AQS通过上述机制提供了一种基础架构,使得开发者可以基于此实现各种复杂的同步组件,如ReentrantLock、Semaphore、CountDownLatch等。
与Synchronized对比
对比项 | synchronized | AQS |
---|---|---|
功能性 | 1. 内置关键字,无需额外引入类库。 2. 支持互斥性和可见性,确保同一时间只有一个线程可以访问同步代码块或方法。 3. 自动管理锁的获取和释放,支持可重入,即一个线程获取到锁后还能再次进入加锁区域。 4. 不提供超时等待锁的功能,且不支持中断请求。 | 1. 是一个底层框架,用于构建更高级别的并发工具如ReentrantLock、Semaphore、CountDownLatch等。 2. 同样支持互斥性和可见性,并通过state变量实现了可重入。 3. 提供了比synchronized更多的功能选项 |
灵活性 | 使用简单,语法直观,但控制粒度相对较粗,只能以整个对象或者方法为单位进行加锁。 | 更灵活,可以通过自定义同步器实现更多定制化的同步需求,比如复杂的条件等待、读写锁等功能。 |
性能 | 在JDK1.6及以后版本中进行了很多优化,如适应性自旋、锁消除、锁粗化等,性能已经相当高,在许多常见场景下与基于AQS的锁性能相近。 | 在某些特定场景下可能有更好的性能表现,如使用自旋锁避免上下文切换,以及通过“工作窃取”算法减少线程间的竞争。 但是,如果使用不当(如过于频繁地创建和销毁AQS实例),可能会导致性能下降。 |
总结
本文对AQS的基本原理和关键代码做了简单解析,同时对比了aqs和synchronized的区别。
创作不易,欢迎一键三连~~