在了解了 AQS
再来学习下,它在 Java
源码中的应用。先从同步工具类开始吧;关于同步工具类如何使用,有一篇很好的文章可以给你:点击这里。
CountDownLatch
允许一个或多个线程等待,直到在其它线程中执行的一组操作完成。
在了解了 AQS
之后,我们知道它的独占锁式和共享锁模式。所以,我们第一步需要弄明白 ,CountDownLatch
使用的是什么模式?参考它的用法知道,它需要一个或多个线程阻塞,并在计数降为 0 后,唤醒所有阻塞的线程,所以,它是一个共享锁模式。
讲 AQS
也提到过,大多数使用它的地方都不是直接继承,而是维护一个内部的私有类来继承,在这里也一样:
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
// 初始化计数,使用 AQS 中的 state
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
/**
* 使用共享锁模式需要实现的方法,在这里,state 为 0 表示获取到锁,如果获取锁失败,将入 CLH 队 * 列并阻塞线程;
*/
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
/**
* 递减 state
*/
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
CountDownLatch
有着以下几个方法,由于每个方法的源码比较简单,这里就不在列出:
-
构造函数:构造函数接受一个 int ,用于创建上面的
Sync
对象,这是 state 初始的值;更改 state 的值必须使用父类的方法,为了线程安全; -
await:该方法调用了
AQS
的acquireSharedInterruptibly
方法,熟悉AQS
的朋友都知道,该方法会回调子类实现的tryAcquireShared
方法(也就是上面的这个方法)。如果这个方法返回的数小于 0,则将入队列,并阻塞,然后等待被唤醒; -
countDwon:该方法调用了
AQS
的releaseShared
方法,每次状态值减一,该方法会回调上面的tryReleaseShared
方法。如果该方法返回 true,将去唤醒队列中阻塞的线程。可以看见,当状态值为已经 0 ,将直接返回 false,只有在状态值第一次等于 0 时,才会返回 true,这时候就会尝试去唤醒队列中的阻塞线程。注意:并不是等于 0 就直接去唤醒,而是只有在第一次等于 0 才会去这样做,也就是后续再调用这个方法并不会再去队列上唤醒线程了,尽管这个队列上没有线程。
-
getCount:返回状态值,在这里的语义可以理解为:等待线程还需再等待多少其它线程完成操作就可以执行。
总结:state 值很重要,只应该通过 AQS
提供的方法来操作它。锁与 state 值只存在一种表示关系,你可以理解为 state 为 0 才表示获得锁,也可以理解为 state 为 1 才表示获得锁,这取决于你自己。只要明白 tryAcquireShared
方法返回一个大于 0 的锁就是锁获取成功,返回小于 1 的数,表示获取锁失败,并且会阻塞等待,其它的完全取决于你的想象力。
CyclicBarrier
允许一组线程彼此等待到达一个共同的障碍点。
CyclicBarrier
有一个代的思想,它指的是当一组线程均到达了屏障,则完成了一代,然后开始下一代;但也有特殊情况,那就是在各线程均到达屏障点以后要执行的那个任务失败了,则将损坏当前代(屏障被打破),其它还未获得执行许可的线程在阻塞超时以后,将得到当前代已经被损坏的信息,并抛出 BrokenBarrierException
异常。
CyclicBarrier
底层是通过 ReentrantLock
,并结合 Condition
来实现的,主要的代码是以下这个方法:
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 代表当前代
final Generation g = generation;
// 检查当前代是否已经损坏
if (g.broken)
throw new BrokenBarrierException();
// 如果当前线程已经被中断,则直接打破屏障
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
int index = --count;
// 代表所有线程均到达屏障
if (index == 0) { // tripped
boolean ranAction = false;
try {
// 所有线程到达屏障后,可以执行的任务
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
// 正常开始下一代,唤醒所有阻塞线程,重置计数和代
nextGeneration();
return 0;
} finally {
// 屏障任务未正常执行完成,将打破屏障,所有阻塞的线程被唤醒,然后,将以异常结束
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
trip.await();
else if (nanos > 0L)
// 到达超时时间也不能结束,需要被唤醒
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
// 阻塞结束后,需要检验的各种条件
if (g.broken)
throw new BrokenBarrierException();
// 当前代已经结束,已正常开始下一代
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
从上面可以知道,如果在屏障处等待的线程被中断或者超时,将打破屏障(如果还处在当前代的话),所有在已到达屏障处的线程将被唤醒,并抛出 BrokenBarrierException
;还未到达屏障处的线程在到达以后,将立即得到该异常。
CyclicBarrier
的方法,都是基于 ReentrantLock
的加锁和解锁来操作的,所以,在调用其它的方法时,也需要注意锁的竞争;
总结:
在屏障处等待的一个线程,有三种结束状态:
- 正常的结束:此时,循环屏障已经开启下一代,可重复使用;
- 被中断,捕获到中断异常:这表明屏障已经被打破,其它在屏障处等待的线程将捕获到
BrokenBarrierException
异常。 - 捕获到
BrokenBarrierException
异常:这可能是由于屏障任务执行失败,其它线程被中断。
想要重用循环屏障,需要调用 reset
方法,不过,参考该方法的实现发现,该方法会先去打破屏障,再开始下一代。该方法的文档介绍说并不适用于重置因某种原因破损的屏障,此时,推荐重新创建循环屏障。
推荐博文
我与风来
认认真真学习,做思想的产出者,而不是文字的搬运工。错误之处,还望指出!
如果觉得这篇文章对你有所帮助,动动小手,点点赞,这将使我能够为你带来更好的文章。