高级并发编程系列十九(并发流程控制之CountDownLatch)

145 阅读4分钟

1.引子

天气稍微有点冷!从本篇文章开始,我们来到高级并发编程系列中的并发流程控制小节了。这里你能先简单思考一下,什么是并发流程控制?为什么需要并发流程控制吗?

我们来简单的捋一下,在我们生活中,合作、协作是办成一件事情的关键。那么并发编程场景一样,多线程一起完成某个任务,自然是需要相互之间的协作了。我们来看几个例子,比如说:

  • 运动会上,百米赛跑运动员起跑前,需要等到发令枪响后,才能一起起跑
  • 同样运动会上,还是百米赛跑,终点裁判员需要等待所有运动员到达后,才能统计排名运动员的成绩
  • 网上购物,凑团拼团,需要凑足多少人后,才能完成拼团订单
  • 热门旅游景点,因为容量有限,需要排队依次有序参观等等

你看到了上面这些我们生活中的例子,相互存在着依赖关系,需要在协作下完成。我们编程中一样,编程最终是为了解决生活中的问题,生活中需要相互协作,线程之间自然也需要相互协作了

问题是有哪些手段,可以让我们在编写并发程序的时候,实现线程之间的协作呢?在Juc包中,给我们提供下列工具:

  • 倒计时门栓CountDownLatch,实现一等多,或者多等一编程模型
  • 循环栅栏CyclicBarrier,实现凑团拼团编程模型
  • 信号量Semaphore,实现热门旅游景点容量有限,需要买票参观编程模型
  • 条件原语Condition,实现生产者消费者编程模型

倒计时门栓、循环栅栏、信号量、条件都是实现线程协作的工具,也是我们这个小节的主要内容。好了,今天就让我们一起先来看倒计时门栓CountDownLatch的使用。

2.案例

2.1.代码演示

案例描述:

  • 模拟百米赛跑中,5个运动员在起跑线做好准备,等待裁判员发起发令枪
  • 当裁判员发起发令枪响后,5个运动员同时起跑
  • 实现并发编程协作中,多等一的编程模型(5个运动员,等待1个发令枪)
/**
 * 线程协作之CountDownLatch
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/1/10 20:39
 */
public class CountDownLatchDemo {
​
    /**
     * 发令枪
     */
    public static CountDownLatch raceCommand = new CountDownLatch(1);
​
    public static void main(String[] args) throws Exception{
​
        // 1.创建5个运动员,一起参加百米竞赛
        Runnable raceTask = new RaceTask();
        for(int i = 0; i < 5; i++){
            new Thread(raceTask, "运动员【" + i + "】").start();
        }
​
        // 2.主线程,通过CountDownLatch的countDown方法(模拟发令枪响)
        Thread.sleep(3);
        System.out.println("------倒数3.2.1后,裁判员发起发令枪.------");
        raceCommand.countDown();
​
    }
​
    /**
     * 百米赛跑运动员
     */
    static class RaceTask implements Runnable{
​
        @Override
        public void run() {
            // 获取运动员名称
            String name = Thread.currentThread().getName();
            System.out.println(name + "准备好了,等待发令枪.");
            try {
                // 通过CountDownLatch的await方法,实现等待发令枪响
                raceCommand.await();
​
                // 运动员起跑,竞赛中(随机休眠时间,模拟运动员跑步花费时间)
                System.out.println("发令枪响了," + name + "起跑.");
                int time = new Random().nextInt(1000);
                Thread.sleep(time);
​
                // 运动员到达终点
                System.out.println( name + "到达终点.共花费时间:" + time + "毫秒.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

执行结果:

运动员【0】准备好了,等待发令枪.
运动员【2】准备好了,等待发令枪.
运动员【3】准备好了,等待发令枪.
运动员【1】准备好了,等待发令枪.
运动员【4】准备好了,等待发令枪.
------倒数3.2.1后,裁判员发起发令枪.------
发令枪响了,运动员【2】起跑.
发令枪响了,运动员【4】起跑.
发令枪响了,运动员【1】起跑.
发令枪响了,运动员【0】起跑.
发令枪响了,运动员【3】起跑.
运动员【0】到达终点.共花费时间:87毫秒.
运动员【3】到达终点.共花费时间:428毫秒.
运动员【1】到达终点.共花费时间:564毫秒.
运动员【4】到达终点.共花费时间:669毫秒.
运动员【2】到达终点.共花费时间:893毫秒.

2.2.关键知识点分析

上面的案例,我们实现了现实生活中,百米竞赛运动员等候发令枪响,多等一的编程模型。你看到通过CountDownLatch实现线程协作,是非常容易的。你需要关注的有三个地方。

  • 构造CountDownLatch对象
// 倒计时门栓,构造函数中的数字值,表示需要倒数的次数
CountDownLatch raceCommand = new CountDownLatch(1);
  • 实现运动员阻塞等待的方法
// 通过CountDownLatch的await方法,实现等待发令枪响
// 调用await方法后,线程阻塞直到倒计时倒数为0后,才能继续运行
 raceCommand.await();
  • 实现发令枪的倒数计数方法
// 2.主线程,通过CountDownLatch的countDown方法(模拟发令枪响)
// 倒数次数,与构造方法中的数字值一致
raceCommand.countDown();