查看原文
其他

死磕Java并发:J.U.C之并发工具类:CountDownLatch

程序猿DD 2019-07-13

作者:chenssy 

来源:Java技术驿站


在上篇博客中介绍了Java四大并发工具一直的CyclicBarrier,今天要介绍的CountDownLatch与CyclicBarrier有点儿相似。


CyclicBarrier所描述的是“允许一组线程互相等待,直到到达某个公共屏障点,才会进行后续任务”,而CountDownLatch所描述的是”在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待“。在API中是这样描述的:


用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。


CountDownLatch是通过一个计数器来实现的,当我们在new 一个CountDownLatch对象的时候需要带入该计数器值,该值就表示了线程的数量。每当一个线程完成自己的任务后,计数器的值就会减1。当计数器的值变为0时,就表示所有的线程均已经完成了任务,然后就可以恢复等待的线程继续执行了。


虽然,CountDownlatch与CyclicBarrier有那么点相似,但是他们还是存在一些区别的:

  1. CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待

  2. CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier



实现分析


CountDownLatch结构如下


通过上面的结构图我们可以看到,CountDownLatch内部依赖Sync实现,而Sync继承AQS。CountDownLatch仅提供了一个构造方法:

CountDownLatch(int count) : 构造一个用给定计数初始化的 CountDownLatch

  1.    public CountDownLatch(int count) {

  2.        if (count < 0) throw new IllegalArgumentException("count < 0");

  3.        this.sync = new Sync(count);

  4.    }


sync为CountDownLatch的一个内部类,其定义如下:

  1. private static final class Sync extends AbstractQueuedSynchronizer {

  2.        private static final long serialVersionUID = 4982264981922014374L;

  3.        Sync(int count) {

  4.            setState(count);

  5.        }

  6.        //获取同步状态

  7.        int getCount() {

  8.            return getState();

  9.        }

  10.        //获取同步状态

  11.        protected int tryAcquireShared(int acquires) {

  12.            return (getState() == 0) ? 1 : -1;

  13.        }

  14.        //释放同步状态

  15.        protected boolean tryReleaseShared(int releases) {

  16.            for (;;) {

  17.                int c = getState();

  18.                if (c == 0)

  19.                    return false;

  20.                int nextc = c-1;

  21.                if (compareAndSetState(c, nextc))

  22.                    return nextc == 0;

  23.            }

  24.        }

  25.    }


通过这个内部类Sync我们可以清楚地看到CountDownLatch是采用共享锁来实现的。

await()

CountDownLatch提供await()方法来使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断,定义如下:

  1.    public void await() throws InterruptedException {

  2.        sync.acquireSharedInterruptibly(1);

  3.    }


await其内部使用AQS的acquireSharedInterruptibly(int arg):

  1.    public final void acquireSharedInterruptibly(int arg)

  2.            throws InterruptedException {

  3.        if (Thread.interrupted())

  4.            throw new InterruptedException();

  5.        if (tryAcquireShared(arg) < 0)

  6.            doAcquireSharedInterruptibly(arg);

  7.    }


在内部类Sync中重写了tryAcquireShared(int arg)方法:

  1.        protected int tryAcquireShared(int acquires) {

  2.            return (getState() == 0) ? 1 : -1;

  3.        }


getState()获取同步状态,其值等于计数器的值,从这里我们可以看到如果计数器值不等于0,则会调用doAcquireSharedInterruptibly(int arg),该方法为一个自旋方法会尝试一直去获取同步状态:

  1.  private void doAcquireSharedInterruptibly(int arg)

  2.            throws InterruptedException {

  3.        final Node node = addWaiter(Node.SHARED);

  4.        boolean failed = true;

  5.        try {

  6.            for (;;) {

  7.                final Node p = node.predecessor();

  8.                if (p == head) {

  9.                    /**

  10.                     * 对于CountDownLatch而言,如果计数器值不等于0,那么r 会一直小于0

  11.                     */

  12.                    int r = tryAcquireShared(arg);

  13.                    if (r >= 0) {

  14.                        setHeadAndPropagate(node, r);

  15.                        p.next = null; // help GC

  16.                        failed = false;

  17.                        return;

  18.                    }

  19.                }

  20.                //等待

  21.                if (shouldParkAfterFailedAcquire(p, node) &&

  22.                        parkAndCheckInterrupt())

  23.                    throw new InterruptedException();

  24.            }

  25.        } finally {

  26.            if (failed)

  27.                cancelAcquire(node);

  28.        }

  29.    }


countDown()

CountDownLatch提供countDown() 方法递减锁存器的计数,如果计数到达零,则释放所有等待的线程。

  1.    public void countDown() {

  2.        sync.releaseShared(1);

  3.    }


内部调用AQS的releaseShared(int arg)方法来释放共享锁同步状态:

  1.    public final boolean releaseShared(int arg) {

  2.        if (tryReleaseShared(arg)) {

  3.            doReleaseShared();

  4.            return true;

  5.        }

  6.        return false;

  7.    }


tryReleaseShared(int arg)方法被CountDownLatch的内部类Sync重写:

  1.    protected boolean tryReleaseShared(int releases) {

  2.        for (;;) {

  3.            //获取锁状态

  4.            int c = getState();

  5.            //c == 0 直接返回,释放锁成功

  6.            if (c == 0)

  7.                return false;

  8.            //计算新“锁计数器”

  9.            int nextc = c-1;

  10.            //更新锁状态(计数器)

  11.            if (compareAndSetState(c, nextc))

  12.                return nextc == 0;

  13.        }

  14.    }


总结

CountDownLatch内部通过共享锁实现。在创建CountDownLatch实例时,需要传递一个int型的参数:count,该参数为计数器的初始值,也可以理解为该共享锁可以获取的总次数。当某个线程调用await()方法,程序首先判断count的值是否为0,如果不会0的话则会一直等待直到为0为止。当其他线程调用countDown()方法时,则执行释放共享锁状态,使count值 – 1。当在创建CountDownLatch时初始化的count参数,必须要有count线程调用countDown方法才会使计数器count等于0,锁才会释放,前面等待的线程才会继续运行。注意CountDownLatch不能回滚重置。


关于共享锁的请参考:【死磕Java并发】—–J.U.C之AQS:同步状态的获取与释放


应用示例

示例仍然使用开会案例。老板进入会议室等待5个人全部到达会议室才会开会。所以这里有两个线程老板等待开会线程、员工到达会议室:

  1. public class CountDownLatchTest {

  2.    private static CountDownLatch countDownLatch = new CountDownLatch(5);

  3.    /**

  4.     * Boss线程,等待员工到达开会

  5.     */

  6.    static class BossThread extends Thread{

  7.        @Override

  8.        public void run() {

  9.            System.out.println("Boss在会议室等待,总共有" + countDownLatch.getCount() + "个人开会...");

  10.            try {

  11.                //Boss等待

  12.                countDownLatch.await();

  13.            } catch (InterruptedException e) {

  14.                e.printStackTrace();

  15.            }

  16.            System.out.println("所有人都已经到齐了,开会吧...");

  17.        }

  18.    }

  19.    //员工到达会议室

  20.    static class EmpleoyeeThread  extends Thread{

  21.        @Override

  22.        public void run() {

  23.            System.out.println(Thread.currentThread().getName() + ",到达会议室....");

  24.            //员工到达会议室 count - 1

  25.            countDownLatch.countDown();

  26.        }

  27.    }

  28.    public static void main(String[] args){

  29.        //Boss线程启动

  30.        new BossThread().start();

  31.        for(int i = 0 ; i < countDownLatch.getCount() ; i++){

  32.            new EmpleoyeeThread().start();

  33.        }

  34.    }}


运行结果: 

- END -

 往期推荐:

  • 死磕Java系列:

  1. 深入分析ThreadLocal

  2. 深入分析synchronized的实现原理

  3. 深入分析volatile的实现原理

  4. Java内存模型之happens-before

  5. Java内存模型之重排序

  6. Java内存模型之分析volatile

  7. Java内存模型之总结

  8. J.U.C之AQS简介

  9. J.U.C之AQS:CLH同步队列

  10. J.U.C之AQS同步状态的获取与释放

  11. J.U.C之AQS阻塞和唤醒线程

  12. J.U.C之重入锁:ReentrantLock

……

  • Spring系列:

  1. Spring Cloud Zuul中使用Swagger汇总API接口文档

  2. Spring Cloud Config Server迁移节点或容器化带来的问题

  3. Spring Cloud Config对特殊字符加密的处理

  4. Spring Boot使用@Async实现异步调用:使用Future以及定义超时

  5. Spring Cloud构建微服务架构:分布式配置中心(加密解密)

  6. Spring Boot快速开发利器:Spring Boot CLI

……

可关注我的公众号

深入交流、更多福利

扫码加入我的知识星球

点击“阅读原文”,看本号其他精彩内容

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存