1.前言
AQS是AbstractQueuedSynchronizer
(抽象同步队列)的简写,它是实现同步器的基础组件,并发包下的锁就是通过AQS实现的。作为开发者可能并不会直接用到AQS,但是知道其原理对于架构设计还是很有帮助的。
今天浅谈一下AQS,为什么是浅谈呢,因为我也仅仅是根据书加上自己的想法来看AQS。
2.LockSupport工具类
在正式讲解AQS之前,我们需要先了解一下LockSupport
类,他的主要作用就是用来阻塞和唤醒线程。
LockSupport
类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport
类的方法的线程是不持有许可证的。LockSupport
是通过Unsafe
类实现的。接下来我们介绍一下LockSupport
类的主要方法。
park()
前面我们已经说过,LockSupport类与每个使用它的线程都会关联一个许可证,如果调用park方法的线程持有许可证,那么就会立马返回,否则就会被阻塞挂起。
System.out.println("begin park!");
LockSupport.park();
System.out.println("end park!");
上述代码会在输出begin park之后进入阻塞状态。因为默认情况下是不具有许可证的。
只有在其他线程调用unpark(Thread t)
方法并且将该线程作为参数,该线程才能返回。如果其他线程调用了该线程的interrupt()方法,设置了中断标志,或者线程被虚假唤醒(parkNanos
方法可以指定阻塞一段时间后自己唤醒,所以可能会出现虚假唤醒!),那么该线程也会返回,所以调用park()方法的时候最好使用循环条件判断方式。
不过使用park阻塞的线程被其他线程中断返回时并不会抛出InterruptedException。
park(Object blocker)
public static void park(Object blocker) {
// 获取调用线程
Thread t = Thread.currentThread();
// Thread类中有一个变量parkBlocker,用来存放park方法传递的blocker对象
// 设置该线程的blocker变量
setBlocker(t, blocker);
// 挂起线程
UNSAFE.park(false, 0L);
// 线程被激活以后清楚blocker变量
setBlocker(t, null);
}
当线程在没有持有许可证调用该方法时,会被阻塞挂起,同时blocker
对象会被记录到线程内部。
我们可以使用LockSupport.park(this)
,这样当程序运行以后,我们使用jstack pid
打印线程堆栈可以查看到具体是哪个类被阻塞了。
unpark(Thread thread)
当一个线程调用unpark的时候,如果作为参数的thread没有持有许可证,则会让线程持有。
如果thread之前因为调用park阻塞挂起,则调用unpark后会被唤醒。
如果thread之前还没有调用park,则在调用unpark以后,如果调用park则会立即返回。
3.AQS
初识AQS
由类图我们可以知道,AQS是一个FIFO双向队列,通过节点head和tail记录队首队尾元素。
Node
Node
节点内部的SHARED
用来标记该线程是在获取共享资源时被阻塞挂起放入AQS队列的,EXCLUSIVE
用来标识该线程是获取独占资源时被阻塞挂起放入AQS队列的。
在Node
节点内部有一个成员变量waitStatus记录当前线程等待状态,可以为CANCELLED
(线程被取消了)、SIGNAL
(线程需要唤醒)、CONDITION
(线程在条件队列里等待)、PROPAGATE
(释放资源时需要通知其他节点)。
ConditionObject
ConditionObject
和Node
一样是AQS的内部类。它用来结合锁实现线程同步,其可以访问AQS的内部变量(state和AQS阻塞队列)。
ConditionObject
是条件变量,每个条件变量对应一个条件队列,我们可以看到ConditionObject
中有两个指针,分别指向条件队列的队尾和队头。条件队列用来存放调用条件变量的await
方法后被阻塞的线程。
state
在AQS中,维护了一个单一变量state,对于不同的实现其有不同的意义:
- 在ReentrantLock中,state表示重入式锁的可重入次数
- 在ReentrantReadWriteLock中,state的高16位用于表示读锁的可获取次数,低16位用于表示写锁的可重入次数。(state的类型是int,占用4个字节)
Shared与Interruptibly
方法名中带有Shared的方法表示获取或释放共享资源,如acquireShared(int arg)
方法名中带有Interruptibly
表示对中断做出响应,当线程调用了带Interruptibly
的方法,如果这时被其他线程中断,那么就会抛出InterruptedException
返回。
AQS工作流程
独占方式下
获取资源
首先使用tryAcquire尝试获取资源,获取成功直接返回,失败则将当前线程封装为EXCLUSIVE
的节点插入到AQS阻塞队列尾部并使用LockSupport.park(this)
挂起自己。
public final void acquire(int arg) {
// tryAcquire的具体实现要由子类来完成,AQS中并不提供实现
if (!tryAcquire(arg) &&
// addWaiter的作用是将当前线程封装为独占类型的节点插入AQS阻塞队列,并且将该节点返回
// acquireQueued
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
释放资源
当一个线程调用 release(int arg)
方法时会尝试使用 tryRelease
操作释放资源,这里是设置状态变量 state 的值,然后调用LockSupport.unpark(thread)
方法激活 AQS 队列里面被阻塞的一个线程 (thread)。被激活的线程则使用tryAcquire尝试,如果条件满足则激活继续向下运行,否则被放入AQS继续阻塞。
public final boolean release(int arg) {
if (tryRelease(arg)) {
// 释放资源成功则尝试激活线程
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
共享方式下
获取资源
线程调用acquireShared(int arg)
获取共享资源时,会首先使用tryAcquireShared
尝试获取资源(修改state的值),成功直接返回,失败则将当前线程封装为Node.SHARED的Node节点插入到AQS阻塞队列的尾部。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
释放资源
当一个线程调用 releaseShared(int arg)
时会尝试使用 tryReleaseShared
操作释放资 源,这里是设置状态变量 state
的值,然后使用 LockSupport.unpark(thread)
激活 AQS 队 列里面被阻塞的一个线程 (thread)。跟独占模式下的流程差不多。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
注意
AQS类并没有提供tryAcquire、tryRelease、tryAcquireShare、tryReleaseShare方法,这些方法都需要子类实现。
同时如果你想使用AQS实现一个自己的锁,那你还需要根据情景重写isHeldExclusively
方法,用来判断锁是被当前线程共享还是独占。
条件变量
条件变量拥有一个条件队列,条件队列要跟AQS阻塞队列区别开。
当多个线程同时调用lock.lock()方法获取锁时,只有一个线程获取到了锁,其 他线程会被转换为 Node 节点插入到 lock 锁对应的 AQS 阻塞队列里面,并做自旋 CAS 尝试获取锁。
如果获取到锁的线程又调用了对应的条件变量的 await() 方法,则该线程会释放获取 到的锁,并被转换为 Node 节点插入到条件变量对应的条件队列里面。