AbstractQueuedSynchronizer源码分析
AbstractQueuedSynchronizer,简称AQS,我们着重关注AQS的两个函数:
- acquire
- release
1.acquire()
1.1 addWaiter函数
private AbstractQueuedSynchronizer.Node addWaiter(AbstractQueuedSynchronizer.Node mode) {
/*
创建一个Node结点
Node的Thread为Thread.currentThread
Node的nextWaiter为独占模式的结点(Node.EXCLUSIVE)
*/
AbstractQueuedSynchronizer.Node node = new AbstractQueuedSynchronizer.Node(Thread.currentThread(), mode );
//将尾节点赋值给pred
AbstractQueuedSynchronizer.Node pred = tail;
//若尾节点不为null 说明Sync队列已经初始化
if (pred != null) {
//新建节点的prev指针指向pred节点(tail)
node.prev = pred;
//通过cas设置node节点为新的尾节点
if (compareAndSetTail( pred, node )) {
//cas设置成功 将旧的尾节点指向node(新的tail)
pred.next = node;
return node;
}
}
//若失败,则进入enq函数
//失败原因:有其他线程抢占到了尾节点或者Sync队列还未初始化成功
enq( node );
return node;
}
图为addWaiter将当前线程封装成Node节点加入Sync队列成功后的Sync队列。
若加入失败,会进入enq(node)函数,我们来看一下该函数的源码:
private AbstractQueuedSynchronizer.Node enq(final AbstractQueuedSynchronizer.Node node) {
for (;;) {
AbstractQueuedSynchronizer.Node t = tail;
//还是先判断Sync队列是否初始化
if (t == null) { // Must initialize
//通过cas设置头节点成功
if (compareAndSetHead(new AbstractQueuedSynchronizer.Node()))
//若成功 则将tail指针也指向头结点
tail = head;
} else {
//否则 通过cas自旋的方式设置尾结点,直至成功返回
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
enq的函数的作用: 若Sync还未初始化,则用cas方式尝试初始化,否则,通过CAS自旋的方式尝试设置该结点为Sync队列的尾结点。
这里我们用图来表示:
- 首先: 若Sync队列尚未初始化,那么先进行一次初始化,如图。
- 初始化完成,CAS自旋的方式尝试设置该结点为Sync队列的尾结点。
注意:我们的所有插入Sync操作都要用cas的方式进行,因为有可能有多个线程在互相竞争。
分析完上面两个函数,addWaiter的作用就是将当前线程封装成Node节点加入Sync队列。
整个流程我们梳理一下:
使用当前线程构造Node,对于一个节点我们需要做的就是将当前节点前驱节点指向尾节点(cur.pre=tail), 尾节点指向它(tail=cur),原来的尾节点的后继节点指向Node. 具体过程:
- 先尝试在队尾快速添加(双向链表保证该操作是O(1)复杂度):
- 假设Sync queue已经初始化(尾节点不为空)
- 分配引用pred指向tail节点.
- 将新建节点的前驱指向尾节点.(cur.pre=pred(old tail))
- 假设cas操作成功, 那么新建节点就变成了新的尾结点.(tail=cur) 此操作一定要是原子操作.因为有可能有多个线程要更新尾节点.
- 将旧的尾节点的后继指针指向新的尾巴节.(pred.next=cur(new tail))
- 若尾节点添加失败(cas失败,说明被其他线程更新了)或者该节点是sync队列的第一个入队节点,那么就进行enq函数.这个过程是无限循环的,所以确保了新建的Node一定可以加入Sync Queue中.
1.2.acquireQueued函数
//Sync队列的结点在独占模式且忽略中断的模式下尝试获取资源
final boolean acquireQueued(final AbstractQueuedSynchronizer.Node node, int arg) {
//标记是否成功获取资源
boolean failed = true;
try {
//标记当前线程是否被中断
boolean interrupted = false;
for (;;) {
//获取当前结点的前驱结点
final AbstractQueuedSynchronizer.Node p = node.predecessor();
//如果前驱结点是头结点,那么当前结点就是老二结点,那便有资格尝试获取资源
if (p == head && tryAcquire(arg)) {
//若获取资源成功 说明head结点已经释放资源
//将当前结点设置为新的头结点
setHead(node);
p.next = null; // help GC
failed = false;
//返回等待获取资源的过程中是否被中断
return interrupted;
}
//若失败,那么说明自己可以休息了,就进入waiting状态,直到被unpark()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
interrupted = true;
}
} finally {
//若在获取资源的过程中发生异常 将进入这里
//若发生异常间并没有获取资源成功 那么将取消获取资源
if (failed)
cancelAcquire(node);
}
}
acquireQueued函数的作用:首先会获取当前节点的前驱节点,如果前驱节点是头结点并且能够成功获取资源, 设置当前结点为头结点,返回。否则,调用shouldParkAfterFailedAcquire和parkAndCheckInterrupt函数。
逻辑流程:
- 获取当前结点的前驱结点
- 判断前驱结点是否是头结点: 头结点的含义就是当前占有锁且正在运行。若前驱结点是头结点且尝试获取资源成功,设置当前结点为头结点。
- 否则的话,进入等待状态。如果没有轮到当前节点运行,那么将当前线程从调度器上摘下,也就是进入等待状态。
如图:
若前驱结点不是头结点或者获取资源失败,那么会进入等待状态,会调用如下函数:
shouldParkAfterFailedAcquire函数
private static boolean shouldParkAfterFailedAcquire(AbstractQueuedSynchronizer.Node pred, AbstractQueuedSynchronizer.Node node) {
//获取前驱结点的状态
int ws = pred.waitStatus;
//如果前驱结点的状态为signal的话 说明前驱结点正在等待被唤醒 那么当前结点就可以安心的进入waiting状态
if (ws == AbstractQueuedSynchronizer.Node.SIGNAL)
return true;
if (ws > 0) {
//如果大于0 说明前驱结点为cancelled被取消,那么就一直往前找,直至找到一个正常状态的结点,并且排在它后面.
do {
node.prev = pred = pred.prev;
//直至找到前驱结点的状态不为cancelled为止
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//为PROPAGATE-3或者是0表示无状态,(为CONDITION-2时,表示此节点在condition queue中)
//如果前驱正常,那么就把前驱节点设置为signal,告诉他释放锁的时候通知一下当前节点。
//设置前驱结点为SINGAL状态
compareAndSetWaitStatus(pred, ws, AbstractQueuedSynchronizer.Node.SIGNAL);
}
return false;
}
shouldParkAfterFailedAcquire函数作用: 只有当前节点的前驱节点为SINGAL时,才可以对该节点所封装的线程进行park操作,否则,不能进行park操作(这里的park操作就是将线程挂起,进入waiting状态,底层实现是Unsafe类的park操作)。
所以我们可以知道,一个状态全部正常的Sync队列应该是下面的结构:
这里我们注意两个地方:
- 如果某结点成为头结点,那么会把头结点的成员变量thread设置为null;
- 只有Sync队列中的最后一个节点的状态为0,其余结点的状态均为signal。
这里我们延续上面的步骤:
假设node的前驱结点为head结点,可是获取资源失败:
此时会进入shouldParkAfterFailedAcquire函数: 由于head结点的waitStatus为0,设置头结点的状态为signal,即
-1,然后返回false,再次进入acquireQueued的循环。
假设再次获取资源失败,那么由于head的状态为-1,那么直接返回true,node结点的线程被park,则进入waiting状态。
以上图示就是整个acquireQueued的流程,直至获取资源成功,或者发生异常。
parkAndCheckInterrupt函数:
private final boolean parkAndCheckInterrupt() {
//将当前线程进行park操作
LockSupport.park(this);
//返回线程是否被中断过 并重置状态
return Thread.interrupted();
}
parkAndCheckInterrupt函数作用: 首先执行park操作,即禁用当前线程,然后返回该线程是否已经被中断。
park()操作会让当前线程进入waiting状态,只有两种途径可以唤醒该线程:
- 被unpark
- 被interrupt().
被unpark的情况我们就不讨论,我们讨论线程在waiting状态中被中断的情况。如果是被中断的,在AQS#acquireQueued()中并不直接响应中断,而是把中断标记清除并记录,在AQS#acquireQueued执行完毕后,把中断记录传递给AQS#acquire(),让AQS#acquire()调用selfInterrupt()重放中断。
这样做的意义何在?为何不直接在AQS#parkAndCheckInterrupt()中直接响应中断。
因为转让中断标志能提供更大的灵活性,外界可以自行决定是及时响应、稍后响应、还是根本就不响应中断。
这里要注意一下:
须知,如果第一次park被中断的话,需要将中断标记重置为fasle,否则第二次park的话会立马被中断(是根据中断标记来确定是否退出waiting状态)
这里AQS将中断上抛的做法就是为了线程在获取锁之后,让我们自己采取措施对中断进行处理。
如果在获取资源的过程中发生异常,且没有成功获取到资源的话,会进入cancalAcquire()方法。
cancalAcquire方法会执行:
- 将node关联的线程断开
- 将node的waitStatus设置为CANCELLED
我们来看一下cancelAcquire这个函数的具体实现:
//取消继续获取资源
private void cancelAcquire(AbstractQueuedSynchronizer.Node node) {
if (node == null)
return;
//将当前结点的thread设置为null
node.thread = null;
AbstractQueuedSynchronizer.Node pred = node.prev;
//找到node前驱节点中第一个状态不为cancelled的结点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
AbstractQueuedSynchronizer.Node predNext = pred.next;
//设置node结点的ws为cancelled
node.waitStatus = AbstractQueuedSynchronizer.Node.CANCELLED;
//如果node结点为尾结点并且cas设置新的尾结点为pred
if (node == tail && compareAndSetTail(node, pred)) {
//若成功,cas设置pred的的下一个结点为null
compareAndSetNext(pred, predNext, null);
} else {
/*
* 若上面步骤失败
* 1、node结点不是尾结点
* 2、在设置尾结点的时候有新的结点加入Sync队列
*/
int ws;
//如果node结点不是tail结点,也不是head的后继结点
//则将node的前继结点的ws设置为signal
//并将pred的next结点指向node的下一个结点
if (pred != head &&
((ws = pred.waitStatus) == AbstractQueuedSynchronizer.Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, AbstractQueuedSynchronizer.Node.SIGNAL))) &&
pred.thread != null) {
//获取node的下一个结点
AbstractQueuedSynchronizer.Node next = node.next;
//设置pred的下一个结点为next
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//否则唤醒该节点的后置结点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
我们分析一下cancelAcquire的三种情况:
- 当node是tail结点的时候,若node出队成功,如下。由于没有任何引用指向node,则node将会被GC回收。
- 当node节点不是tail结点,且node的前驱结点不是head的结点的时候,如下:
注意:此时node结点并没有真正的出队被gc回收,它还有successor的前驱指针指着,导致它还处于GC roots可达状态。
那么node结点什么时候处于真正意义上的出队呢,我们想想我们什么时候才会清除这些状态为CANCELLED的结点,其实在shouldParkAfterFailedAcquire和cancelAcquire都有处理这些waitStatus为CANCELLED的结点的步骤。例如:
//shouldParkAfterFailedAcquire
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//cancelAcquire
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
这两个函数处理waitStatus为CANCELLED的地方,仅仅需要断开与前继结点状态为cancalled的联系,那么相关waitStatus为cancelled的结点就真正意义上的出队(已没有任何引用指向它)了。
- 当node的前驱结点是头结点
由于当前结点是cancelled状态,说明node结点所绑定的线程已经放弃对资源的争抢,那么就有必要将资源让给后面的结点,由于node的前驱结点是head结点,那么就尝试唤醒node的后继结点进行资源的争抢。
我们来看一下unparkSuccessor函数:
private void unparkSuccessor(AbstractQueuedSynchronizer.Node node) {
int ws = node.waitStatus;
if (ws < 0)
//将状态设置为0
compareAndSetWaitStatus(node, ws, 0);
AbstractQueuedSynchronizer.Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//从后往前查找最前一个状态正常的结点
for (AbstractQueuedSynchronizer.Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//对其进行唤醒
if (s != null)
LockSupport.unpark(s.thread);
}
其实unparkSuccessor函数作用就是唤醒node之后第一个状态正常的结点。
以上就是cancelAcquire的全部内容,我们来整理一下流程:
- 断开node与其内部Thread的联系
- 如果node结点是尾结点,那么直接设置让node出队。
- 如果node的前驱结点不是头节点且node不是尾结点,那么就将node的前驱结点的后继指针指向node的后继结点。
- 如果node的前驱结点是头结点,那么就唤醒后面的结点(该点是node之后第一个状态正常的结点)。
至此,acquireQueued的整个流程已经讲完了。我们来梳理一下整个aquireQueued的流程:
- 判断结点的前驱是否为head并且是否成功获取资源。
- 若1均满足,则设置节点为头结点,之后会判断finally块并返回。
- 若1不满足,则判断是否需要park当前线程,是否需要park当前线程的逻辑是判断结点的前驱结点是状态是否为 signal。若是,则park当前结点。否则,不进行park操作。
- 若park当前线程,之后某个线程对本线程unpark后,并且本线程也获得运行机会。那么,将会继续进行步骤1判断。
2.release
public final boolean release(int arg) {
//若尝试是否资源成功
if (tryRelease(arg)) {
AbstractQueuedSynchronizer.Node h = head;
//若头结点不为空并且头结点的状态不为0
if (h != null && h.waitStatus != 0)
//唤醒head后面的结点
unparkSuccessor(h);
return true;
}
return false;
}
release做的事很简单,就是唤醒head结点后面的结点,而unparkSuccessor上面已经讲过了,这里就不再分析了。
这里我们画一个图吧,其实还是和上面的图有联系的。
这个图就是当前线程从获取锁失败加入到CLH同步队列,直至获取锁的全部过程。
以上所讲,便是AQS独占锁实现的源码的全部分析。