假设把一次计算拆成三个任务,交给三个线程执行,主线程需要等它们都结束后再汇总结果。除了分别调用 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 更适合参与者数量会变化的多阶段任务,支持注册、到达和注销。使用时要特别注意注册与退出配对,否则一个已经不再参与的任务会让整轮一直等它。