Aqs有两种模式,一种独占模式,一种共享模式,他们获取资源的方法分别对应
acquire-release、acquireShared-releaseShared(见此博文 https://blog.csdn.net/a6822342/article/details/84875304)
这篇文章我们来看看独占模式下的获取资源和释放资源的代码。
先来看acquire的源码
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先获取尝试获取资源(这个tryAcquire需要子类自己实现,可以参考java.util.concurrent.locks.ReentrantLock.FairSync这里面的实现去理解),如果获取到就直接返回了,如果没有获取到,就把该个线程看成阻塞队列里面的一个结点加入到队列中去,也就是addWaiter方法做的事情,然后就在队列里面等待获取资源,acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
下面先看看addWaiter的源码
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
这里有个难点,就是它首先会执行快速入队的代码,也就是if语句里面的代码,如果它符合要求,则快速入队成功,如果不行,那么就执行正常入队的方法enq。
疑点1:为什么执行快速入队的时候会有可能不成功呢?
疑点2:enq(node)的里面的代码的代码逻辑和快速入队的代码逻辑差不多,为什么会有两段这种类似的代码呢?
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize 代表着初始队列为空,需要初始化
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
关于以上两个疑问的解释,在该篇博客里面有写道,请各位看官移步:
https://www.jianshu.com/p/c806dd7f60bc
相信看了上面这篇博客,各位应该对addWaiter方法有一定的了解了。
下面我们来看看acquireQueued(Node, int)方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC,头结点直接置null
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
我们已经知道该线程已经获取资源失败,被加到了阻塞队列的尾部,那么接下来就是进入等待休息状态,等到队列前面的线程都完成了自己的事情,把资源释放出来之后就可以获取在资源,然后干自己的事情了。
我们看acquireQueued方法里面的failed变量一开始设置为true,代表着没有获取到资源,Interrupted代表在等待的过程中是否被中断。
在for循环中,首先获得该线程的前一个结点,看它是不是头结点,如果是头结点的话,那么该线程作为阻塞队列里面的第二个结点就有资格去获取资源了,然后通过tryAcquire去看有没有获取到资源,如果该线程的前一个结点是头结点的话并且自己也获取到资源的话,则把自己设为头结点,把之前的头结点设置为null,使其阻塞队列中移除。然后failed设置为false,return interrupted则是表明自己在等待的过程中有没有被中断过。
等待过程的代码是:
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
这里是通过shouldParkAfterFailedAcquire和parkAndCheckInterrupt来判断自己是不是可以进入休息状态了,如果在休息状态的时候被中断过,则把interrupted设置为true,然后当自己成为头结点的时候返回。
下面我们看看shouldParkAfterFailedAcquire和parkAndCheckInterrupt都做了些什么。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;//这一块应该会把队列里面的CANCEL状态的结点都给剔除
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
/*
* 这里的状态除了是0就是PROPAGATE,则将前驱结点的状态设置为SIGNAL,
* 但是返回的是false,不让其休息,可能是在这段时间内,已经处理到该结
* 点的前驱结点了,如果是的话,它就不用休息了,下一步就轮到它获得资源了
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这里需要看结点的waitstatus状态,这里说明一下:
waitStatus则表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。
CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,将其结点的waitStatus改为CANCELLED,进入该状态后的结点将不会再变化。具有该状态的线程永远不会再次阻塞。
SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点需要被唤醒,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。
CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition队列上。在传输之前,它不会用作同步队列节点,此时状态将设置为0.(此处使用此值与字段的其他用法无关,但可简化机制。)--代码中的直译
PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
0状态:值为0,代表初始化状态。
AQS在判断状态时,通过用waitStatus>0表示取消状态,而waitStatus<0表示有效状态
那么我们回来看代码
int ws = pred.waitStatus; 首先拿到前驱的状态,如果前驱的状态是singnal,那么该结点就可以去休息了,只要等前驱做完事情之后唤醒自己一下就可以了,如果前驱的状态大于0,则根据上面的状态表表示它是CANCELLED,所以需要去找前面有没有一个正常的状态,也就是下面这行代码的内容:node.prev = pred = pred.prev; pred指向pred的prev,然后当前结点的前驱指向pred的前驱(跳过了当前结点的前驱结点),直到找到这么一个结点,然后把该节点的next指向node,则中间的不符合状态的结点都会断链(如CANCEL),然后被GC
Else语句中的这个compareAndSetWaitStatus(pred, ws, Node.SIGNAL)是在前驱结点的状态的值大于0的情况下才执行的,将前驱结点的状态变为signal,这样当前驱结点拿到资源的时候,就会去唤醒当前结点,这样当前结点就可以放心去休息了。
如果前驱的当前状态小于0,这里的状态除了是0就是PROPAGATE,则将前驱结点的状态设置为SIGNAL,但是最后返回的是false,则代表着不让其休息,可能是在这段时间内,已经处理到该结//点的前驱结点了,如果是的话,它就不用休息了,下一步就轮到它获得资源了(个人猜测)。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。
到此acquireQueued()方法分析完成,总结下该函数的具体流程:
1.结点进入队尾后,检查状态,找到安全休息点;
2.调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;
3.被唤醒后,看自己是不是有资格能拿到号(还在acquireQueued的自旋for循环里面)。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1
下面我们来看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;
}
首先执行tryRelease方法,如果成功,则执行if条件语句里面的方法,唤醒下一个后继结点。
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
tryRelease的方法需要子类去实现(可以参考java.util.concurrent.locks.ReentranLock.Sync去理解),如果释放资源成功(独占模式一般都会成功),返回true,不成功返回false。
下面进if条件语句里面看一看,首先获得头结点,然后如果它不空且它的状态不为0(结点的初始化状态为0,不为0的状态在独占模式下要么为CANCEL和SINGNAL,CANCEL的话这种状态的结点应该是在acquire的时候给剔除了,因为shouldParkAfterFailedAcquire这个方法会判断当前结点的前驱结点,然后符合规则的置waitstatus为SINGNAL,不符合的则继续往前找,直到找到之后将该前驱结点的next指向当前结点,则中间的结点都会断链,然后被GC。SINGNAL状态代表着该结点的后续结点需要被唤醒)的话,就调用unparkSuccessor方法。下面来看看unparkSuccessor方法。
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
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);
}
这块先获得node的状态,也就是release的node h。首先判断它是不是小于0,小于0则代表它的状态为SINGNAL,然后将其状态用CAS转为初始状态,因为node这个节点的线程释放了锁后续不需要做任何操作,接着去唤醒后继结点。
然后去找它的后继结点,如果它的状态是大于0,那么只能是CANCEL状态。或者它的结点为null,那么就从队列的最后面往前去找离当前结点最近的状态小于等于0的结点,然后去唤醒它。
参考文章:
https://www.cnblogs.com/waterystone/p/4920797.html
http://ifeve.com/juc-aqs-reentrantlock/
https://www.jianshu.com/p/6afaef97264a
到这里独占模式的acquire-release方法就已经分析完了,希望大家喜欢。有什么不足之处,欢迎指教~