AQS - 同步状态的获取与释放(独占式)
-
同步状态的获取与释放2大类,共享式和独占式,主要针对的就是第一篇文章中所描述的几个独占式模板方法。
表格所示如下: -
子类重写的流程方法所示如下:
方法作用 | 独占式 |
---|---|
获取同步状态 | tryAcquire(int arg) |
释放同步状态 | tryRelease(int arg) |
判断是否被当前线程所独占 | isHeldExclusively() |
- AQS中模板方法所示如下:
方法作用 | 独占式 |
---|---|
获取同步状态 | acquire(int arg) |
获取同步状态(可响应中断) | acquireInterruptibly(int arg) |
获取同步状态(可超时) | tryAcquireNanos(int arg, long nanos) |
释放同步状态 | release(int arg) |
- 在同步状态的获取与释放的部分,AQS采用了模板方法的设计模式,子类继承ASQ然后重写对应的流程方法,AQS中的模板方法会调用对应重写的流程方法,线程队列的处理等细节已经
在AQS中实现好了。AQS面对的是锁和其他同步组件的实现者,继承AQS可以很方便的实现自定义的锁。
一、独占式获取同步状态
- 下面我们分析独占式获取共享状态的方法中,有不支持中断的,支持中断和支持超时参数的,这些方法的都在AQS中实现了基本的方法流程骨架,将不能确定的逻辑部分再流程方法中
由子类实现,因此交给流程方法由子类实现。
1.1 不支持中断的acquire(int arg)
1.1.1 acquire(int arg)
- AQS提供的模板方法。该方法为独占式获取同步状态,对中断不敏感。也就是说,由于线程获取同步状态失败而加入到CLH 同步队列中,后续对该线程进行中断操作时,线程不会从CLH
同步队列中移除。代码如下:
public final void acquire(int arg) {
//1.tryAcquire尝试获取同步状态,成功就直接返回,否则会调用tryAcquire直到成功。
//2.addWaiter方法在第二篇文章已分析过,是将当前线程封装为一个Node对象,然后加入到同步队列,EXCLUSIVE表示独占。
//3.acquireQueued是自旋的过程,因为tryAcquire失败了,所以就把当前线程封装为Node假如队列,然后当前线程就会
// 一直自旋,直到获取到同步状态再返回
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1.1.2 acquireQueued
- 这是一个自旋的过程,也就是说,当前线程(Node)进入同步队列后就会不断自旋,每个节点都会自省地观察,当条件满足,获取到同步状态后,就可以从这个自旋过程中退出,否则会一直执行下去;
final boolean acquireQueued(final Node node, int arg) {
// 记录是否获取同步状态成功
boolean failed = true;
try {
//2.记录过程中,是否发生线程中断
boolean interrupted = false;
//3.自旋过程,其实就是一个死循环而已
for (; ; ) {
//4.当前线程的前驱节点
final Node p = node.predecessor();
//5.当前线程的前驱节点是头结点,说明自己倒了队列第一的位置,如果此时还获取同步状态成功,那就
// 成功了
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//6.如果前驱不是头结点,那就先进行节点状态的更新(shouldParkAfterFailedAcquire方法)
// 然后调用parkAndCheckInterrupt阻塞当前线程,如果过程中被中断过,会在interrupted变量中记录
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//7.如果一直没有自旋到成功(自己没能成为head的后继),那就取消
if (failed)
cancelAcquire(node);
}
}
1.1.3 shouldParkAfterFailedAcquire
- 这个方法里面会维护Node里面的状态,他首先会判断前驱的节点是不是正常的SIGNAL,是的话,自己就可以安安心心的等待了;
如果是取消状态,那么就跳过已经取消的前驱节点,如果状态不对,比如是初始状态,就修改为SIGNAL
/**
* //acquire获取同步状态失败后,在该方法内部进行状态的更新和检查,如果应该阻塞则返回true
*/
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.
* SIGNAL表示后面的节点可以阻塞,等待前面的通知
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
* 大于0表示状态是:CANCELLED,说明前驱等待超时或者中断了,那么这里就循环往前面遍历,将那
* 些CANCELLED状态的节点移除队列,一直到找到小于0的状态为止
*/
do {
node.prev = pred = pred.prev;
} 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.
* ws是0或者PROPAGATE,说明我们需要一个signal,但是线程还没有阻塞,本次执行会返回false
*因此在acquireQueued中会自旋再次调用,这里将状态修改为 Node.SIGNAL ,
* 下一次自旋就会满足第一个if的判断逻辑了
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
/**
* 这个方法主要是将当前线程park阻塞,如果阻塞被唤醒之后,就会判断自己是不是被中断的(判断中断标志位被置是否为true,
* 因为中断也会让线程从park中退出),如果是被中断了,那就会返回true,并且将标志位改为false(改为false后线程会继续运行)
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
1.1.4 cancelAcquire
- 取消尝试获取同步状态
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
//1.节点不存在就自然取消
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
//2.跳过已经取消的前驱节点,一直往前面寻找 waitStatus<=0的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
//predNext会记录下往前面回溯到的Node的后继,
//比如当前几个节点的waitStatus依次是:-1 , 1 ,2 ,0 (0是node,值只是举例,主要是为了跳过大于0的节点)
//这个while回溯的过程会将pred指向-1的节点,然后predNext就是指向1的节点
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
//将状态设置为取消状态
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
//如果自己是尾节点了,直接移除即可,CAS保证线程安全
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
//如果自己不是尾节点,那么自己取消了,需要告诉后继节点
int ws;
//Condition1:前驱不是head
//Condition2:前驱状态ws是SIGNAL 或者ws<0但将其ws设置为SIGNAL成功
//Condition3:前驱的线程不是null,head的thread域是null
//三者需要同时满足,意思就自己是处于队列中的节点(没有紧挨head),并且前驱的thread不是null,
//并且前驱的ws是SIGNAL(表示后续需要等待唤醒,这样自己取消之后,也不影响自己后面的节点成为前驱的后继)
//那么此时自己就可以取消了,逻辑里面将自己的后继设置为前驱的后继,自己安安心心退出
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL || //前驱的状态不是SIGNAL
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))//
&& pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);//将前驱和后继建立连接关系,
} else {
//如果pred为首节点,则唤醒下一个节点的等待线程,为什么呢?因为每一个节点在阻塞等待的时候,都需要它的
//前驱在释放同步信号时唤醒它,可是现在pred是首节点,意味着node是队列首节点,node现在取消获取
//同步状态,就不会再释放信号唤醒它后面那个阻塞的节点了,因此这里需要对后继进行唤醒
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
1.2 支持中断的acquireInterruptibly(int arg)
- AQS的acquire(int arg)是独占式获取同步状态,但是该方法对中断不响应,对线程进行发出中断信号之后该线程会依然位于CLH同步队列中等待着获取同步状态。为了响应中断,AQS 提供了acquireInterruptibly(int arg)方法。该方法在等待获取同步状态时,如果当前线程被中断了,会立刻响应中断,并抛出InterruptedException 异常。
/**
* 支持中断的acquireInterruptibly,如果当前线程被中断了,会立刻响应中断,并抛出 InterruptedException 异常
*/
public final void acquireInterruptibly(int arg) throws InterruptedException {
//1.在内部源码中Thread.interrupted()的调用很常见,判断当前线程中断标记是否为true,并且为true的话,
//会将标记置为false,然后抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
//2.如果没有中断的话,调用的是子类重写的流程方法tryAcquire,如果获取成功就返回,如果获取失败就
// 执行doAcquireInterruptibly(arg),里面其实也是自旋
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
1.2.1 doAcquireInterruptibly
- 这也是是一个自旋的过程,过程和不支持中断响应的acquireQueued几乎一样,就只是在被中断的时候逻辑不一样,acquireQueued不接受中断响应,其实
内部捕获到中断异常之后,通过一个布尔值记录并且返回了,但是doAcquireInterruptibly则会抛出异常,其他的逻辑几乎是一样的。
/**
* 独占式支持中断的同步状态获取,和不支持响应的时候很类似,
*/
private void doAcquireInterruptibly(int arg) throws InterruptedException {
//1.打包Node节点
final Node node = addWaiter(Node.EXCLUSIVE);
//2.记录是否成功
boolean failed = true;
try {
//3.自旋过程,其实就是一个死循环而已
for (; ; ) {
//4.拿到前驱节点
final Node p = node.predecessor();
//5.当前线程的前驱节点是头结点,说明自己倒了队列第一的位置,如果此时还获取同步状态成功,那就
// 成功了
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
//6.如果前驱不是头结点,那就先进行节点状态的更新(shouldParkAfterFailedAcquire方法)
// 然后调用parkAndCheckInterrupt阻塞当前线程,如果过程中被中断过,抛出异常
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
//7.如果一直没有自旋到成功(自己没能成为head的后继),那就取消
if (failed)
cancelAcquire(node);
}
}
1.3 支持超时的tryAcquireNanos(int arg, long nanos)
- 支持超时的独占式同步状态获取,
/**
* 支持超时的独占式同步状态获取,
*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
//1.中断,抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//2.尝试获取成功,返回true
//否则调用doAcquireNanos进入支持超时的获取逻辑
return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
1.3.1 doAcquireNanos(arg, nanosTimeout)
- doAcquireNanos和doAcquireInterruptibly、acquireQueued在逻辑上区别不大,前2者只是在acquireQueued在基础只是增加了响应中断和支持超时的功能,
(支持超时通常也是要支持中断的),只是在捕获到异常信号之后的处理逻辑和自旋的循环中增加对超时的判断,另外还考虑到了锁的粗化,避免不必要的线程
park。
/**
* 支持超时的独占式获取更新状态,自旋直到获取成功或者超时
*/
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
//1.超时时间不能为负数
if (nanosTimeout <= 0L)
return false;
//2.计算绝对超时时间deadline
final long deadline = System.nanoTime() + nanosTimeout;
//3.打包Node节点
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
//4.死循环自选,直到超时或者成功
for (; ; ) {
final Node p = node.predecessor();
//5.当前线程的前驱节点是头结点,说明自己倒了队列第一的位置,如果此时还获取同步状态成功,那就
// 成功了
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
//6.不能成功获取,就计算剩余的超时时间
nanosTimeout = deadline - System.nanoTime();
//7.已经超时了就直接返回
if (nanosTimeout <= 0L)
return false;
//8.没有超时,那就先进行节点状态的更新(shouldParkAfterFailedAcquire方法)
//如果剩余的超时时间超过1000纳秒,那就把线程park,如果不足1000,那就不会park,会进入下一次
//的自旋,看这个变量的注释,应该是小与1000的时候自选效率更高,park组合和唤醒线程的话,或许系统
// 开销会更大,(锁粗化)
if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
//9.被中断抛出异常
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
//10.取消
if (failed)
cancelAcquire(node);
}
}
二、独占式释放同步状态
2.1 release(int arg)
- release方法相对简单,他说基于流程方法tryRelease来实现的,释放成功之后,唤醒后继节点。
/**
* 独占模式下释放同步状态变量
*/
public final boolean release(int arg) {
//1.如果tryRelease释放成功
if (tryRelease(arg)) {
Node h = head;
//2.head释放state之后,就可以唤醒head的后继节点线程执行了
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
三、总结
- 在AQS中使用了模板方法模式,子类重写流程方法,逻辑框架在AQS中已经实现好,简化了子类的实现难度,在后面的文章中我们会使用AQS来实现自定义的
锁,在之前的代码分析中我们也看到AQS在显式锁和CountDownLatch中的应用。 - 我们看到在基准模式的获取方式中,流程上几乎是一样的。对比不支持中断的模式,支持中断的模式就是在收到中断响应的时候抛出异常,而前者只是将中
断用一个布尔变量记录下来而已,而支持超时的,在自旋的时候,每次都会检查剩余的超时时间,稍微增加了部分超时的逻辑判断,大体上都差不多。在第三
篇文章"03-Java多线程、线程等待通知机制"中提到的超时模式范式,和源码中几乎是一样的。 - 取消同步状态的获取时,也需要考虑比较多的情况,如果自己是尾节点就比较简单,如果自己不是尾节点就需要唤醒后继节点,并且需要考虑前驱后继节点
状态不对的时候,需要修改状态或者移除相关的Node节点,同时需要将自己的前后对应节点链接起来。 - 同步状态的获取都需要考虑线程安全,因此采用了CAS和自旋操作,同步状态的释放因为不是并发操作,因此简单很多。关于共享式的同步状态获取和释放在
下一篇文章中分析。