概要
AQS,全称AbstractQueuedSynchronizer,抽象队列同步器,是Java多线程中的一个基础类,为诸多多线程工具类(如CountDownLatch,CyclicBarrie、ReentrantLock等)提供了基础框架,本文主要对AQS内部实现做了比较深入的分析
数据模型
Node节点
Node类是AQS中实现的一个内部类,用于包装线程以及线程状态的表示,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。
- CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
- SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
- CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
- 0:新结点入队时的默认状态。
注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
// ...
}
CLH队列
CLH队列是AQS里面维护的一个双向链表,链表的每个节点是一个Node对象,抢占锁失败的线程就会加入到该链表中
ConditionObject
ConditionObject实现了Condition接口,主要用于AQS中的条件等待,每个ConditionObject都会维护一个链表,其中节点也是上述的Node示例,用于表示处在当前条件等待队列中的线程,但不同于CLH队列的是,这里的链表只是一个单向链表
重点方法分析
acquire(int)
方法流程如下:
- tryAcquire方法(该方法在AQS中默认抛出异常,需要子类根据自己的需求重写此方法)尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待)
- addWaiter方法将该线程加入等待队列的尾部,并标记为独占模式;
- acquireQueued方法使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。下面是acquire()的源码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(int)
尝试去获取独占资源,如果获取成功,则直接返回true,否则直接返回false。
//默认抛出异常,具体实现交由具体的同步器根据业务需求去实现
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
addWaiter(Node)
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {
//自旋
//获得尾结点
Node oldTail = tail;
if (oldTail != null) {
//如果尾节点不为空说明现在队列已初始化,直接放入到队尾
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
//通过cas将尾节点修改为node
oldTail.next = node;
return node;
}
} else {
//如果尾节点为空说明队列中没有结点,需要初始化
initializeSyncQueue();
}
}
}
acquireQueued(Node,int)
流程:
- 结点进入队尾,查看自己的前驱节点是否是头节点,如果是,则再次尝试获取锁
- 从后往前查看当前CLH队列中的节点,直到找到节点的waitStatus<=0才结束,这里waitStatus>0表示该线程取消,需要清理掉这些节点
- 调用park进入waitting状态,等待unpark()或interrupt()唤醒自己
- 被唤醒后,查看是否可以获取资源,如果拿到,head指向当前结点,并返回从入队拿到号的整个过程中是否被中断过;如果没拿到,继续流程1.
通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入到等待队列尾部了。此时线程需要将自己挂起,直到其他线程释放资源后唤醒自己,自己拿到资源后,再去执行临界区代码。
自旋:这里这个循环一般来说都会执行两次。
第一次,线程进入shouldParkAfterFailedAcquire会去寻找第一个可用的前驱节点并修改该节点的waitStatus为SIGNAL(用来后面唤醒自己)并清除状态为Cancel的节点。
第二次,再次进入shouldParkAfterFailedAcquire(),此时当前节点的前驱节点的状态已经被修改为SIGNAL,函数返回true,然后执行parkAndCheckInterrupt将自己挂起。
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false; //标记是否被中断过
try {
//自旋!!
for (;;) {
//拿到前驱结点
final Node p = node