Java 并发基础(三):再谈 CountDownLatch

你好,我是看山。

java.util.concurrent.CountDownLatch 是 JDK 1.5 提供的一个同步辅助类:在一组正在其他线程中的操作执行完成之前,它允许一个或多个线程一直等待。

初始化 CountDownLatch 时需要指定计数。通过调用 countDown 方法使当前计数到达零之前,await 方法会一直阻塞。之后,所有等待的线程会被释放,await 后面的操作也会立即执行。因为计数无法被重置,所以这种操作只会出现一次。如果需要重置计数,请考虑使用 java.util.concurrent.CyclicBarrier。

CountDownLatch 是一个通用同步工具,它有很多用途。用 1 初始化的 CountDownLatch 可以用作一个简单的开/关锁存器或入口:在某一线程调用 countDown 打开入口前,所有调用 await 的线程都一直在入口处等待。用 N(N>=1) 初始化的 CountDownLatch 可以使一个线程在 N 个线程完成某项操作之前一直等待,或者使其在某项操作完成 N 次之前一直等待。

CountDownLatch 的一个有用特性是,在计数达到 0 之前,它不会阻塞调用 countDown 方法的线程继续执行,它只是阻止所有调用 await 的线程继续执行 await 后面的操作。

CountDownLatch 有两种典型用法:

  1. 有两个计数器,一个启动信号,一个完成信号。
  2. 将一个问题分成 N 个部分,需要执行的 N 个子部分定义为 Runnable,然后将所有 Runnable 加入到 Executor 队列中。当所有的 N 个子部分完成后,等待的线程将会通过 await 方法继续执行后续操作。

两个计数器

示例代码:

public class CountDownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch startSignal = new CountDownLatch(1);// 先行条件
        CountDownLatch doneSignal = new CountDownLatch(N);

        for (int i = 0; i < N; ++i) {
            new Thread(new Worker(startSignal, doneSignal)).start();
        }

        doSomethingElse();
        startSignal.countDown();// 开始
        doSomethingElse();
        doneSignal.await();// 等待所有操作结束
    }
}

class Worker implements Runnable {
    private final CountDownLatch startSignal;
    private final CountDownLatch doneSignal;

    Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
        this.startSignal = startSignal;
        this.doneSignal = doneSignal;
    }

    @Override
    public void run() {
        try {
            startSignal.await();// 等待先行条件结束
            doWork();
            doneSignal.countDown();
        } catch (InterruptedException ex) {
        }
    }

    void doWork() {...}
}

在示例代码中,startSignal 为先决条件,此处初始计数为 1,是一个简单的开关。比如田径比赛中的百米跑,发令枪响前,运动员都在等待;发令枪响后,运动员开始比赛,等到最后一名运动员到达终点,比赛结束。

简单的代码实现为:

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class CountDownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        final CountDownLatch begin = new CountDownLatch(1);
        final CountDownLatch end = new CountDownLatch(3);
        final ExecutorService exec = Executors.newFixedThreadPool(3);

        for (int index = 0; index < 3; index++) {
            exec.submit(new Runner(index + 1, begin, end));
        }

        System.out.println("各就各位。");
        System.out.println("预备。");
        System.out.println("嘭。");
        begin.countDown();
        end.await();
        System.out.println("比赛结束,准备颁奖。");
        exec.shutdown();
    }
}

class Runner implements Runnable {
    private int no;
    private CountDownLatch begin;
    private CountDownLatch end;

    public Runner(int no, CountDownLatch begin, CountDownLatch end) {
        this.no = no;
        this.begin = begin;
        this.end = end;
        System.out.println("No." + no + "到达起跑线。");
    }

    @Override
    public void run() {
        try {
            begin.await();// 等待发令枪响
            System.out.println("No." + no + "向前飞奔着。");
            TimeUnit.SECONDS.sleep(new Random().nextInt(10));// 奔跑的过程中。
        } catch (InterruptedException e) {
        }
        System.out.println("No." + no + "到达终点。");
        end.countDown();
    }
}

引申开来,开始定义一系列的执行的链条,第一个没有先决条件,直接执行,第二个以第一个为先决条件,以此类推。我是懒人,不做太多赘述,我一个朋友的文章中写的不错:利用 CountDownLatch 同步辅助类进行线程同步

一个计数器

一个计数器的情况自己感觉情况比较单一,就是主线程等待子线程结束,再继续执行。这里的主线程、子线程是相对而言的,可能主线程本身是另一个线程的子线程。

示例代码:

public class CountDownLatchTest6 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch doneSignal = new CountDownLatch(3);
        ExecutorService e = Executors.newCachedThreadPool();

        for (int i = 0; i < 3; ++i) {
            e.execute(new Worker(doneSignal, i));
        }
        doSomethingElse();
        doneSignal.await();// 等待子线程结束
        doSomethingElse();
        e.shutdown();
    }
}

class Worker implements Runnable {
    private final CountDownLatch doneSignal;
    private final int id;

    Worker(CountDownLatch doneSignal, int id) {
        this.doneSignal = doneSignal;
        this.id = id;
    }

    @Override
    public void run() {
        doWork();
        doneSignal.countDown();
    }

    void doWork() {...}
}

示例代码中的 doSomethingElse 方法可以是一些业务逻辑代码,根据具体功能发生变化。

对于这种方式可以参看上一篇中关于老板和工人的例子中的第二种解决方法 主线程等待子线程结束,这里也不做赘述。

推荐阅读


你好,我是看山,公众号:看山的小屋,10 年老猿,开源贡献者。游于码界,戏享人生。

个人主页:https://www.howardliu.cn
个人博文:Java 并发基础(三):再谈 CountDownLatch
CSDN 主页:http://blog.csdn.net/liuxinghao
CSDN 博文:Java 并发基础(三):再谈 CountDownLatch

公众号:看山的小屋