AQS源码的文章想必大家已经看了很多,但是可能还是似懂非懂,这里讲一下如何快速理解AQS的原理及AQS到底有什么用。
先来点几个前提知识:
AQS中有三个原子操作的方法
- getState()
- setState()
- compareAndSetState()
AQS是个abstract抽象类,但是实际却没有任何抽象方法需要实现,当我们尝试重写AQS的方法时候,发现大多数方法是private和final修饰的,只有这几个方法能够重写:
总共有7个方法可以重写,其中有框内的四个是我们重写的比较多的,而且这个重写是强制性的,因为这4个方法都是抛出运行时异常
当然不是说4个都需要重写,用AQS编写独占模式的功能要重写tryRealse,tryAcquire,而编写共享模式需要重写tryReleaseShared, tryAcquireShared。
也就是说使用AQS来实现功能就是写一个类继承AQS并重写这4个方法(或其中两个)。AQS这个就是一个模板模式。AQS中有许多方法已经自己完成,剩下4个可以由代码编写者自主编写。(AQS自己已完成的方法中会用到这4个方法)。
好了我们开始将如何理解AQS。
AQS的独占模式的运行过程其实是类似于去饭堂的一个窗口打饭。
一个人要去窗口打饭,他走到窗口尝试打饭,这时候窗口如果没有人,他可以成功占有打饭的权利,只要饭堂有饭,他就是第一打饭的,如果这时候窗口已经有人了,他是想插队会被人揍,他打不到饭,他没有打饭的权利,他只能回到队伍最后面等待。那这个人什么时候有机会打饭?只有等到队伍前面的人打完了他才有机会打饭,那么问题是队伍最前面的那个人什么时候打饭? 队伍最前面的那个人会在饭堂阿姨将饭成功拿出来,阿姨说抢饭的时候,被叫醒然后开始打饭,如果打到饭了,他就走了,他走了,他后面的那个人看到他走了,也会醒来抢饭(不需要阿姨叫他抢饭),如果打到饭了,就离开继续叫醒下一个打饭…直到有人抢饭的时候发现没饭了,整个队伍又进入了等待状态,直到阿姨下一次成功拿出饭,叫大家抢饭,队伍再次进入抢饭的动态中。(这个过程中还会有人不断尝试)饭堂阿姨的成功上饭,会使队伍进入动态打饭中,没饭了会使整个队伍进入等待中。
【我们惊奇的发现,从某种意义上来讲,只要我们控制了饭堂阿姨,就能控制能走几个人,哪些人不能走,控制了整个人流】
AQS的这个模型对我们编程的意义是什么?
AQS主要用来实现对线程的流量控制,数量控制,执行顺序。线程因为其特殊性,在不使用JUC的情况下,很难控制其执行顺序,线程阻塞,唤醒都很麻烦(或者很损耗性能)。多线程就像是下班高峰时期挤地铁公交,是混乱的,没法控制。有了AQS后,我们可以从更多方面更灵活的操纵多线程。模型中排队的人其实就是排队的线程,我们可以更灵活控制线程的阻塞或者运行。
我们在上个模型中可以控制两个点,1.当排队者要打饭的时候,我们可以决定,他可不可以打到饭(如果让排队者打到饭,他就会走,不让他就会进入等待状态)。2.我们可以决定饭堂阿姨能不能上饭。
表现在独占模式下AQS中就是tryAcquire和tryRelease这两个方法的逻辑由我们控制。共享模式就是tryAcquireShared和tryReleaseShared。
由于我们常用AQS对线程数量进行监控,AQS还提供了线程安全的计数工具—getState,setState,compareAndSetState。
好接下来我们就以CountDownLatch来分析一下以上模型。
CountDownLatch的使用如下:
用10个线程countDown,主线程await阻塞 直到10个线程全部countDown,await才会被唤醒。那么执行结果应该是这样:
那我们现在想想如何用AQS实现这么一个功能,首先我们需要一个计数器记录countDown的次数,AQS自带state给我们做计数的使用,再者await需要阻塞,类似AQS中的排队打饭的人,我们可以控制上饭成不成功,我们只需要在计数没有达到10个之前一直不上饭,await就会一直阻塞,直到最后一个线程countDown完成,唤醒排队打饭的线程,并让其打饭成功就能走了。
但是要注意tryAcquire tryRelease应该是内部实现不应对外暴露,所以常常被设计在内部类里面,对外提供的方法 是 acquire和release。
下面来源码分析:
public class CountDownLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
private final Sync sync;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void countDown() {
sync.releaseShared(1);
}
public long getCount() {
return sync.getCount();
}
public String toString() {
return super.toString() + "[Count = " + sync.getCount() + "]";
}
}
上面是源码,我们按照例子里面的执行顺序来分析, 首先主线程进入await()
public void await() throws InterruptedException {
// 注意方法里面数字参数在这里没有太大的意义 改成0改成5都行,
// 因为这个参数主要传递到tryAcquireShared中
// 而在countdownlatch的tryAcquireShared中没有利用到这个参数
// acquireShared 与 acquireSharedInterruptibly 他们两个都是打饭的方法
// 区别是 一个线程中断不了,一个可以中断
// 开始打饭!
sync.acquireSharedInterruptibly(1);
}
下面是打饭的具体逻辑
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
// 如果线程被设为中断状态了 这里会抛出异常线程被打断
// 这个是 Interruptibly的特性 在这里可以忽略
if (Thread.interrupted())
throw new InterruptedException();
// 重点是这里 打饭的流程还记得吗
// 我们先尝试去打饭 如果打不到饭 (返回数值小于0) 我们就去排队 否则打完走人
// 在我们上面那个例子里 这个时候应该是打不到饭的 要进入队列排队
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg); // 打不到饭, 老实排队
// 打到饭了 走人不用阻塞了!
}
能不能打到饭的逻辑是我们自己实现的,在countDownLatch的逻辑应该是countDown的次数没有达到指定次数就不能打到饭,我们来看这个打不打的到饭的具体代码
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
// 这里的参数是实例化CountDownLatch时传递的那个数值
// 按照我们上面的代码示例来说 这里是10
Sync(int count) {
// state被初始化为了10
setState(count);
}
// 打不打得到饭得逻辑在这里决定 返回小于0得值 代表没有打到饭,大于0得值代表打到饭了
// 留个思考题返回值为什么不直接用boolean和tryAcquire一样? boolean只能有两种状态 int可以表示更多得含义
// 具体得话 应该跟都独占模式和共享模式下要处理得问题稍微不同有关
// 好,我们按照我们上面得例子 主线程调用 await-》acquireShared 来打饭了,但是此时如果countDown还未被执行
// 此时state是10 返回结果是 -1 打饭失败
// 主线程进入等待队列的休眠状态(park)
// 主线程要如何被唤醒(unpark)再次进行打饭还记得吗?只有等饭堂阿姨大叫一声 抢饭拉!
// 主线程才会重新打饭
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
}
兵分两头正当我们主线程达不到饭阻塞在队列中休眠得时候,支线程终于开始countDown。
// 开始countDown
public void countDown() {
// release和releaseShared 就是 饭堂阿姨进行上饭的方法
// 这里的参数1 在这里没有作用 改成其他的也可以 因为参数直接传到 tryReleaseShared
// 但是tryReleaseShared 里的这个参数也没用上 (其实可以用上)
// 上饭! 上饭的流程还记得吗,上饭也可以分为两种情况
// 尝试上饭失败和尝试上饭成功
// 上饭失败的话后续没有任何的操作了
// 上饭成功的话 饭堂阿姨就会叫开饭啦 把等待队列中的线程叫醒 让其尝试打饭
sync.releaseShared(1);
}
继续看上饭流程
// 上饭!
public final boolean releaseShared(int arg) {
// 上饭失败的话 返回false 后续的程序也不会再进行任何操作
// 上饭成功的话 开饭啦! 唤醒队列里的等待线程打饭
// 按照我们的例子 这里我们必须要countdown10次才可以上饭成功
if (tryReleaseShared(arg)) {
doReleaseShared(); // 上饭成功 开始唤醒
return true;
}
return false;
}
我们来看如何才是上饭成功
// 尝试上饭 这里是volatile + CAS自旋实现 类似加锁修改state
protected boolean tryReleaseShared(int arg) {
// 上饭流程 countDown -> releaseShared -> tryReleaseShared
// 最开始如果(只)有一个countDown线程进来 此时state 是 10
// 会将其修改为9(如果) 此时 9 != 0 返回false 代表上饭失败,不会唤醒等待队列
// 下一个countdown再进来 下一个再进来 当总共有10个countDown线程进来过后此时state终于被修改为了0
// 返回 true 表示上饭成功! 唤醒队列 队列被唤醒后第一件事就是尝试打饭
//
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
// 上饭成功后 被阻塞的主线程终于被唤醒 开始尝试打饭
// 此时state是0 返回 1 表示打饭成功
// 主线程将会dequeue 退出队列 进行后续执行
// 按照acquireSharedInterruptible->await进行返回 await最终从阻塞中唤醒出来 countdownlatch的使命完成
// (如果此时 state还不是0 主线程又进入等待中)
protected int tryAcquireShared(int arg) {
return getState() == 0 ? 1 : -1;
}
上面就是粗略的CountDownLatch的整个运行过程。
AQS在大多数JUC中被用到都是利用state记录线程的个数作为阻塞好还是唤醒线程的依据,acquire/acquireShared阻塞线程,release/releaseShared唤醒线程。
acquire-release模式可以简单理解为线程的阻塞-唤醒模式。
大家可以继续思考一下ReentrantLock,Semaphore等用AQS如何实现,然后用源码验证下自己的猜想。