假设把一次计算拆成三个任务,交给三个线程执行,主线程需要等它们都结束后再汇总结果。除了分别调用 join(),还可以用 JUC 提供的 CountDownLatch。

CountDownLatch

CountDownLatch 维护一个倒计数。countDown() 减一,减到零后,等待中的 await() 才能通过。

import java.util.concurrent.CountDownLatch;

public class LatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch done = new CountDownLatch(3);
        int[] results = new int[3];
        for (int i = 0; i < results.length; i++) {
            final int index = i;
            new Thread(() -> {
                try {
                    results[index] = (index + 1) * 10;
                } finally {
                    done.countDown();
                }
            }).start();
        }
        done.await();
        System.out.println(results[0] + results[1] + results[2]);
    }
}

结果是 60。每个线程写不同的数组位置,倒计数前的写入在 await 成功返回后可见。

把 countDown() 放进 finally,是为了某个任务失败时也能表示“这个任务已经结束”。但计数归零并不等于业务全部成功,异常和任务结果仍要单独收集。倒计数到零后不能恢复成初始值。

CyclicBarrier

CyclicBarrier 让参与者互相等待,一批线程都调用 await 后,才一起进入下一步。它可以反复使用,适合分阶段计算。

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class BarrierDemo {
    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier barrier = new CyclicBarrier(2,
                () -> System.out.println("本轮到齐"));
        Runnable task = () -> {
            try {
                for (int round = 0; round < 2; round++) barrier.await();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } catch (BrokenBarrierException e) {
                throw new IllegalStateException(e);
            }
        };
        Thread a = new Thread(task);
        Thread b = new Thread(task);
        a.start();
        b.start();
        a.join();
        b.join();
    }
}

输出两次 本轮到齐。两个线程每轮都要等对方到达,才能继续执行。某个等待者中断或超时,会打破当前屏障,其他等待者会收到 BrokenBarrierException,因此异常处理也需要考虑这一轮任务如何结束。

Semaphore

假设最多允许 5 个任务同时执行,可以创建许可数为 5 的 Semaphore。任务先调用 acquire() 获取许可,成功后进入 try 块执行,并在 finally 中调用 release() 归还许可。如果获取时被中断,就没有拿到许可,此时再调用 release() 会额外增加许可数。

它没有锁的持有者概念,可以由不同线程释放。因此需要应用自己遵守配对规则。许可数为 1 也不等于可重入锁,同一线程再次 acquire 仍可能把自己等住。

限制“同时有 5 个任务运行”可以用信号量;限制“每秒最多 5 个请求”还需要时间维度,两者并不是一回事。

Exchanger 和 Phaser

Exchanger 让两个线程交换数据,一方到达后等待另一方配对,适合两个缓冲区交替交付的场景。配对线程可能不来时,应使用带超时的交换并处理失败。

Phaser 更适合参与者数量会变化的多阶段任务,支持注册、到达和注销。使用时要特别注意注册与退出配对,否则一个已经不再参与的任务会让整轮一直等它。

最后修改:2018 年 03 月 20 日
如果觉得我的文章对你有用,请随意赞赏