AQS的快速理解与实战应用

AQS源码的文章想必大家已经看了很多,但是可能还是似懂非懂,这里讲一下如何快速理解AQS的原理及AQS到底有什么用。
先来点几个前提知识:
AQS中有三个原子操作的方法

  1. getState()
  2. setState()
  3. 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如何实现,然后用源码验证下自己的猜想。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值