zoukankan      html  css  js  c++  java
  • 并发——深入分析CountDownLatch的实现原理

    一、前言

      最近在研究java.util.concurrent包下的一些的常用类,之前写了AQSReentrantLockArrayBlockingQueue以及LinkedBlockingQueue的相关博客,今天这篇博客就来写一写并发包下的另一个常用类——CountDownLatch。这里首先要说明一点,CountDownLatch是基于AQS实现的,AQS才是真正实现了线程同步的组件,CountDownLatch只是它的使用者,所以如果想要学习CountDownLatch,请一定先要弄懂AQS的实现原理。我以下的描述均建立在已经了解AQS的基础之上。我之前写过一篇AQS实现原理的分析博客,感兴趣可以看一看:并发——抽象队列同步器AQS的实现原理


    二、正文

    2.1 抽象队列同步器AQS

      在说CountDownLatch前,必须要先提一下AQSAQS全称抽象队列同步器(AbstractQuenedSynchronizer),它是一个可以用来实现线程同步的基础框架。当然,它不是我们理解的Spring这种框架,它是一个类,类名就是AbstractQuenedSynchronizer,如果我们想要实现一个能够完成线程同步的锁或者类似的同步组件,就可以在使用AQS来实现,因为它封装了线程同步的方式,我们在自己的类中使用它,就可以很方便的实现一个我们自己的锁。

      AQS的实现相对复杂,无法通过短短的几句话将其说清楚,我之前专门写过一篇分析AQS实现原理的博客:并发——抽象队列同步器AQS的实现原理

      在阅读下面的内容前,请一定要先学习AQS的实现原理,因为CountDownLatch的实现非常简单,完全就是依赖于AQS的,所以我以下的描述均建立在已经理解AQS的基础之上。可以阅读上面推荐博客,也可以自己去查阅相关资料。


    2.2 CountDownLatch的实现原理

      既然已经开始学习CountDownLatch的实现原理了,那一定已经知道了它的作用,我这里就不详细展示了,简单介绍一下:CountDownLatch的被称为门栓,可以将它看成是门上的锁,它会给门上多把锁,只有每一把锁都解开,才能通过。对于线程来说,CountDownLatch会阻塞线程的运行,只有当CountDownLatc内部记录的值减小为0,线程才能继续向前执行。

      CountDownLatch底层通过AQS实现,AQS的一般使用方式就是以内部类的形式继承它,CountDownLatch就是这么使用它的。在CountDownLatch内部有一个内部类Sync,继承自AQS,并重写了AQS加锁解锁的方法,并通过Sync的对象,调用AQS的方法,阻塞线程的运行。我们知道,创建一个CountDownLatch对象时,需要传入一个整数值count,只有当count被减小为0时线程才能通过await方法,否则将被await阻塞。这里实际上是这样的:当线程运行到await方法时,需要去获取锁(锁由AQS实现),若count不为0,则线程就会获取锁失败,被阻塞;若count为0,则就能顺利通过CountDownLatch是一次性的,因为没有方法可以增加count的值,也就是说,一旦count被减小为0,则之后就一直是0了,也就再也不能阻塞线程了。下面我们就从源码的角度来分析CountDownLatch


    2.3 CountDownLatch的内部类

      前面我们说过,CountDownLatch内部定义了一个内部类Sync,继承自AQS,通过这个内部类来实现线程阻塞,下面我们就来看一看这个内部类的实现:

    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
    
        /** 构造方法,接收count值,只有count减小为0时,线程才不会被await方法阻塞 */
        Sync(int count) {
            // CountDownLatch利用AQS的方式就是直接让count作为AQS的同步变量state
            // 所以直接用state记录count值
            setState(count);
        }
    
        /** 获取当前的count值 */
        int getCount() {
            return getState();
        }
    
        /** 
         * 这是AQS的模板方法acquireShared、acquireSharedInterruptibly等方法内部将会调用的方法,
         * 由子类实现,这个方法的作用是尝试获取一次共享锁,对于AQS来说,
         * 此方法返回值大于等于0,表示获取共享锁成功,反之则获取共享锁失败,
         * 而在这里,实际上就是判断count是否等于0,线程能否向下运行
         */
        protected int tryAcquireShared(int acquires) {
            // 此处判断state的值是否为0,也就是判断count是否为0,
            // 若count为0,返回1,表示获取锁成功,此时线程将不会阻塞,正常运行
            // 若count不为0,则返回-1,表示获取锁失败,线程将会被阻塞
            // 从这里我们已经可以看出CountDownLatch的实现方式了
            return (getState() == 0) ? 1 : -1;
        }
    
        /**
         * 此方法的作用是用来是否AQS的共享锁,返回true表示释放成功,反之则失败
         * 此方法将会在AQS的模板方法releaseShared中被调用,
         * 在CountDownLatch中,这个方法用来减小count值
         */
        protected boolean tryReleaseShared(int releases) {
            // 使用死循环不断尝试释放锁
            for (;;) {
                // 首先获取当前state的值,也就是count值
                int c = getState();
                // 若count值已经等于0,则不能继续减小了,于是直接返回false
                // 为什么返回的是false,因为等于0表示之前等待的那些线程已经被唤醒了,
                // 若返回true,AQS会尝试唤醒线程,若返回false,则直接结束,所以
                // 在没有线程等待的情况下,返回false直接结束是正确的
                if (c == 0)
                    return false;
                // 若count不等于0,则将其-1
                int nextc = c-1;
                // compareAndSetState的作用是将count值从c,修改为新的nextc
                // 此方法基于CAS实现,保证了操作的原子性
                if (compareAndSetState(c, nextc))
                    // 若nextc == 0,则返回的是true,表示已经没有锁了,线程可以运行了,
                    // 若nextc > 0,则表示线程还需要继续阻塞,此处将返回false
                    return nextc == 0;
            }
        }
    }
    

      可以看到,内部类Sync的实现非常简单,它只实现了AQS中的两个方法,即tryAcquireShared以及tryReleaseShared,这两个方法是AQS提供的使用共享锁的接口。这也就表明,CountDownLatch实际上是一种共享锁机制,即锁可以同时被多个线程获取,这个不难理解,因为一旦count被减小为0,则所有线程通过await方法时,都能够顺利通过,不会因为获取不到锁而阻塞。而且从上面的实现中我们可以看到,Sync直接将count值作为AQSstate的值,只有state的值为0,线程才能获取锁,也就是获得执行权限。


    2.4 CountDownLatch的成员变量和构造方法

      下面来看一看CountDownLatch的属性和构造方法:

    /**
     * 只有一个成员变量,就是内部类Sync的一个对象,通过此对象调用AQS的方法,实现线程阻塞和唤醒
     */
    private final Sync sync;
    
    
    /**
     * 只有一个构造方法,接收一个count值
     */
    public CountDownLatch(int count) {
        // count值不能小于0
        if (count < 0) throw new IllegalArgumentException("count < 0");
        // 直接创建一个Sync对象,并传入count值,Sync内部将会执行setState(count)
        this.sync = new Sync(count);
    }
    

    2.5 await方法分析

      CountDownLatch类最最核心的两个方法就是await以及ountDown,我们先来看一看await方法的实现:

    // 此方法用来让当前线程阻塞,直到count减小为0才恢复执行
    public void await() throws InterruptedException {
        // 这里直接调用sync的acquireSharedInterruptibly方法,这个方法定义在AQS中
        // 方法的作用是尝试获取共享锁,若获取失败,则线程将会被加入到AQS的同步队列中等待
        // 直到获取成功为止。且这个方法是会响应中断的,线程在阻塞的过程中,若被其他线程中断,
        // 则此方法会通过抛出异常的方式结束等待。
        sync.acquireSharedInterruptibly(1);
    }
    

      await的实现异常简单,只有短短一行代码,调用了AQS中已经封装好的方法。这就是AQS的好处,AQS已经实现了线程的阻塞和唤醒机制,将实现的复杂性隐藏,而其他类只需要简单的使用它即可。为了方便理解,我们还是来看看acquireSharedInterruptibly方法吧:

    /** 此方法是AQS中提供的一个模板方法,用以获取共享锁,并且会响应中断 */
    public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
        // 首先判断当前线程释放被中断,若被中断,则直接抛出异常结束
        if (Thread.interrupted())
            throw new InterruptedException();
        
        // 调用tryAcquireShared方法尝试获取锁,这个方法被Sycn类重写了,
        // 若count == 0,则这个方法会返回1,表示获取锁成功,则这里会直接返回,线程不会被阻塞
        // 若count < 0,将会执行下面的doAcquireSharedInterruptibly方法,
        // 此处请去查看Sync中tryAcquireShared方法的实现
        if (tryAcquireShared(arg) < 0)
            // 下面这个方法的作用是,线程获取锁失败,将会加入到AQS的同步队列中阻塞等待,
            // 直到成功获取到锁,而此处成功获取到锁的条件就是count == 0,若当前线程在等待的过程中,
            // 成功地获取了锁,则它会继续唤醒在它后面等待的线程,也尝试获取锁,
            // 这也就是说,只要count == 0了,则所有被阻塞的线程都能恢复运行
            doAcquireSharedInterruptibly(arg);
    }
    

      相信看到这里,对CountDownLatch的实现原理已经有一个比较清晰的理解了。CountDownLatch的实现完全就是依赖于AQS的,所有再次提醒,如果以上内容理解不了,请先去学习AQS


    2.6 countDown方法分析

      下面我们来分析CountDownLatch中另一个核心的方法——countDown

    /**
     * 此方法的作用就是将count的值-1,如果count等于0了,就唤醒等待的线程
     */
    public void countDown() {
        // 这里直接调用sync的releaseShared方法,这个方法的实现在AQS中,也是AQS提供的模板方法,
        // 这个方法的作用是当前线程释放锁,若释放失败,返回false,若释放成功,则返回false,
        // 若锁被释放成功,则当前线程会唤醒AQS同步队列中第一个被阻塞的线程,让他尝试获取锁
        // 对于CountDownLatch来说,释放锁实际上就是让count - 1,只有当count被减小为0,
        // 锁才是真正被释放,线程才能继续向下运行
        sync.releaseShared(1);
    }
    

      为了方便理解,我们还是来看一看AQSreleaseShared方法的实现:

    public final boolean releaseShared(int arg) {
        // 调用tryReleaseShared尝试释放锁,这个方法已经由Sycn重写,请回顾上面对此方法的分析
        // 若tryReleaseShared返回true,表示count经过这次释放后,等于0了,于是执行doReleaseShared
        if (tryReleaseShared(arg)) {
            // 这个方法的作用是唤醒AQS的同步队列中,正在等待的第一个线程
            // 而我们分析acquireSharedInterruptibly方法时已经说过,
            // 若一个线程被唤醒,检测到count == 0,会继续唤醒下一个等待的线程
            // 也就是说,这个方法的作用是,在count == 0时,唤醒所有等待的线程
            doReleaseShared();
            return true;
        }
        return false;
    }
    

    三、总结

      如果直接去看CountDownLatch的源码会发现,它的实现真的非常简单,包括注释在内,总共300行代码,除去注释,连100行代码都不到。因为它所作的工作,除了重写AQS的两个方法外,其余的基本上就是调用AQS提供的模板方法而已。所以,理解CountDownLatch的过程,实际上是理解AQS的过程,只要理解了AQS,看懂CountDownLatch的原理,不需要5分钟。AQS真的是Java并发中非常重要的一个组件,很多类都是基于它实现的,比如还有ReentrantLock,同时AQS也是面试中的常考点,所以一定要好好研究。最后再次推荐我之前编写的有关AQS的源码分析博客:并发——抽象队列同步器AQS的实现原理


    四、参考

    • JDK1.8源码
  • 相关阅读:
    批量修改横断面图高程范围
    VS添加命令直接创建pkt文件
    Msi中文件替换
    Vs2015 当前不会命中断点,没有与此关联的可执行代码
    纵断面图标注栏数据复制
    批量修改曲面样式中的显示模式
    《AutoCAD Civil 3D .NET二次开发》勘误2
    AutoCAD .NET Wizard下载地址
    样例文件C3DCustomUI无法编译、加载
    angular2 datePipe IOS不兼容问题
  • 原文地址:https://www.cnblogs.com/tuyang1129/p/12692423.html
Copyright © 2011-2022 走看看