理解Java并发工具类CountDownLatch

时间:2022-06-11
本文章向大家介绍理解Java并发工具类CountDownLatch,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

CountDownLatch相信大家并不陌生,我们在上篇文章中已经分析其实现,这里在简单回顾一下CountDownLatch是基于AQS共享锁构建的一种同步器,它的主要应用场景有两种:

(1)一个线程等待所有的其他线程执行完任务之后自己再执行自己的任务。

(2)多个线程等待一个线程执行完任务之后,然后多个线程同时开始执行自己的任务。

在实际开发中,可能大家仅仅对第一种场景比较熟悉,而完全忽视了第二种场景,实际上第二种场景才是CountDownLatch发挥共享锁的真正案例。

CountDownLatch的方法主要是:

(1)构造方法:

CountDownLatch(int count)
参数count控制线程的数量

(2)await()
阻塞当前调用的线程,直到count的值等于0才唤醒,除非执行了线程中断,否则
在没到达0之前,一直处于waiting状态

(3)await(long timeout, TimeUnit unit)

阻塞当前调用的线程,直到count的值等于0才唤醒,除非执行了线程中断或者指定的时间周期过期,否则在没到达0之前,一直处于waiting状态

(4)countDown()
每次调用对count的值减1,当这个值到达0的时候,会释放所有等待的线程。


(5)getCount()
 返回当前count的数量

下面我们看一个比较典型的一个例子:

package concurrent.tools;

import java.util.Random;
import java.util.concurrent.CountDownLatch;

/**
 * Created by Administrator on 2018/8/20.
 */
public class CountDownDemo2 {





    static class Worker implements Runnable{

        private final CountDownLatch startSignal;
        private final CountDownLatch dongSignal;

        Worker(CountDownLatch startSignal,CountDownLatch dongSignal){
            this.startSignal=startSignal;
            this.dongSignal=dongSignal;
        }
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName()+"  启动了,等待main线程调度.......");
                startSignal.await();
                doWork();
                System.out.println(Thread.currentThread().getName()+"  完活 ..... ");
                dongSignal.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }


        void doWork() throws InterruptedException {

            System.out.println(Thread.currentThread().getName()+"  开始工作 ..... ");
            Thread.sleep(5000);

        }
    }



    public static void main(String[] args) throws InterruptedException {


        CountDownLatch startSignal=new CountDownLatch(1);//
        CountDownLatch doneSignal=new CountDownLatch(5);

        for(int i=0;i<5;i++){

            new Thread(new Worker(startSignal,doneSignal)).start();
        }
        Thread.sleep(4000);
        System.out.println(Thread.currentThread().getName()+"线程准备就绪,所有线程可以开始工作了..... ");
        startSignal.countDown();
        doneSignal.await();

        System.out.println(Thread.currentThread().getName()+"线程监控任务结束 ");






    }
}

执行完成之后,输出结果如下:

Thread-0  启动了,等待main线程调度.......
Thread-2  启动了,等待main线程调度.......
Thread-1  启动了,等待main线程调度.......
Thread-3  启动了,等待main线程调度.......
Thread-4  启动了,等待main线程调度.......
main线程准备就绪,所有线程可以开始工作了..... 
Thread-0  开始工作 ..... 
Thread-2  开始工作 ..... 
Thread-4  开始工作 ..... 
Thread-3  开始工作 ..... 
Thread-1  开始工作 ..... 
Thread-2  完活 ..... 
Thread-3  完活 ..... 
Thread-4  完活 ..... 
Thread-1  完活 ..... 
Thread-0  完活 ..... 
main线程监控任务结束

上面的例子就是一个非常典型的例子,反应到实际生活的场景比如,张三生日party,准备在一个大酒店进行,客房里面有4个服务员等待服务上菜,前提是张三所有的朋友必须到齐才能开始宴会,然后张三就是协调者,在所有朋友到齐之后,张三发话开始上菜,这时候4个服务员就可以去同时进行上菜。这个例子里面就和上面我们代码执行的例子非常类似。此外还有在web服务器中,必须等缓存初始化之后,我们的程序才对外提供服务,那么这个场景也可以使用CountDownLatch来完成。

这里大家需要避免一个误区,大多数时候我们都是多个线程调用 countDown,只有一个线程调用await, 但实际情况是await方法也是可以有多个线程调用的,而这正是共享锁的体现。

关于CountDownLatch使用的几个步骤:

(1)构造函数指定需要等待的线程数量

(2)对于执行countDown方法的线程为了安全起见这个调用必须写在finally块里面,防止线程发生异常退出,导致程序永远不会终止。

(3)对于异常终止判断,我们可以通过一个布尔变量或者CountDownLatch的getCount方法来判断是不是有的任务异常退出,从而决定需要做什么

@Override protected void onKernalStart0() 
                    throws IgniteCheckedException {
    try {
        queueHdrView = cctx.cache();
        initFlag = true;
    }
    finally {
        initLatch.countDown();
    }
}

(4)对于执行await方法的线程,我们需要判断是否有效,如果无效则要抛出终端异常。

public static void await(CountDownLatch latch) 
              throws IgniteInterruptedCheckedException {
    try {
        if (latch.getCount() > 0)
            latch.await();
    }
    catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new IgniteInterruptedCheckedException(e);
    }
}

最后需要注意的是CountDownLatch仅仅只能被用一次,不能被重置,如果需要循环重置则需要使用Java并发工具包的另外一个类CyclicBarrier。这个会在下一篇文章中介绍。