CountDownLatch 原理

候选人小蒋在面试美团 P6 时,面试官问道:

"CountDownLatch 有什么用?它和 CyclicBarrier 有什么区别?"

小蒋说:"CountDownLatch 用于等待一组操作完成..."面试官追问:"它是怎么实现的?为什么叫倒计时门栓?"

小蒋答不上来。面试官继续:"countDown() 之后,await() 的线程一定会被唤醒吗?"

小蒋彻底卡住了...

一、核心问题:CountDownLatch 原理 🔴

1.1 问题拆解

第一层:使用场景(怎么用?)
  "CountDownLatch 的典型使用场景是什么?"
  考察点:主线程等待、多子任务并行

第二层:AQS 共享实现(怎么做到?)
  "CountDownLatch 的 await() 和 countDown() 底层是怎么实现的?"
  考察点:AQS 共享模式、state 递减

第三层:与 CyclicBarrier 的区别(有什么不同?)
  "CountDownLatch 和 CyclicBarrier 的本质区别是什么?"
  考察点:一次性 vs 可复用、谁等谁

第四层:局限性(有什么不能做的?)
  "CountDownLatch 能被重置吗?"
  考察点:一次性、不可复用

1.2 ❌ 错误示范

候选人原话 A:"CountDownLatch 和 CyclicBarrier 是一样的,只是名字不同。"

问题诊断:两者有本质区别。CountDownLatch 是一次性的,计数到零后不能重置;CyclicBarrier 是可复用的, parties 个线程全部通过后自动重置。

候选人原话 B:"CountDownLatch 计数到零后,所有等待的线程都会被唤醒。"

问题诊断:这个理解基本正确,但不够精确——只有调用了 await() 的线程会被唤醒。未调用 await() 的线程不受影响。

1.3 标准回答

P5 级别:使用场景

典型场景:主线程等待子任务完成

CountDownLatch latch = new CountDownLatch(3);

// 主线程等待
latch.await();  // 阻塞,直到计数为 0

// 子任务 1
new Thread(() -> {
    doWork();
    latch.countDown();  // 计数 -1
}).start();

// 子任务 2
new Thread(() -> {
    doWork();
    latch.countDown();
}).start();

// 子任务 3
new Thread(() -> {
    doWork();
    latch.countDown();
}).start();

常见使用场景

  • 启动多个服务,等待所有服务就绪后再开始处理
  • 分布式计算中,主节点等待所有子节点完成计算后汇总结果
  • 游戏加载:等待所有资源加载完成后进入游戏

P6 级别:AQS 共享实现

核心实现

CountDownLatch 内部使用 AQS 的共享模式state 字段存储倒计时计数:

// CountDownLatch.Sync
private static final class Sync extends AbstractQueuedSynchronizer {
    Sync(int count) {
        setState(count);
    }

    // tryAcquireShared: 计数为 0 才能获取
    protected int tryAcquireShared(int ignores) {
        return getState() == 0 ? 1 : -1;
    }

    // tryReleaseShared: 计数 -1,如果为 0 返回 true
    protected boolean tryReleaseShared(int releases) {
        for (;;) {
            int c = getState();
            if (c == 0) return false;  // 已经是 0,不能再 release
            int nextc = c - 1;
            if (compareAndSetState(c, nextc))
                return nextc == 0;  // 返回 true 时唤醒等待线程
        }
    }
}

await() 的流程

public final void acquireSharedInterruptibly(int arg) {
    if (tryAcquireShared(arg) < 0) {  // state != 0,获取失败
        if (Thread.interrupted()) throw new InterruptedException();
        doAcquireSharedInterruptibly(arg);  // 阻塞,加入共享队列
    }
}

countDown() 的流程

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {  // 递减 state,如果为 0
        doReleaseShared();  // 唤醒共享队列中的所有节点
        return true;
    }
    return false;
}

为什么叫"门栓"

"门栓"(Latch)是硬件中的一个概念——门栓关闭时阻止通过,门栓打开时允许通过。CountDownLatch 在计数未到零时阻止 await() 的线程通过,计数到零时打开门栓。

P7 级别:与 CyclicBarrier 的区别

本质区别

维度CountDownLatchCyclicBarrier
复用性一次性,计数到零后不能重置可复用,parties 个线程全部通过后自动重置
等待方向一方等待另一方:子任务执行 countDown,主线程执行 await多方互相等待:所有参与方互相等待
线程关系独立线程,无相互等待N 个线程互相等待
状态不可重置可重置(通过 reset()
典型场景"主线程等待 N 个子任务完成""N 个线程互相等待,到齐后一起执行"

CyclicBarrier 的内部实现

public class CyclicBarrier {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition trip = lock.newCondition();
    private final int parties;
    private int count;

    public int await() throws InterruptedException {
        lock.lock();
        try {
            int index = --count;
            if (index == 0) {  // 最后一个到达
                trip.signalAll();  // 唤醒所有等待线程
                count = parties;  // 重置
                return 0;
            }
            // 等待其他线程
            trip.await();
            return index;
        } finally {
            lock.unlock();
        }
    }
}

使用对比

// CountDownLatch:主线程等待子任务
// 场景:启动检查(多个服务就绪后主服务开始)
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch doneLatch = new CountDownLatch(SERVICE_COUNT);

for (int i = 0; i < SERVICE_COUNT; i++) {
    final int id = i;
    new Thread(() -> {
        // 等待开始信号
        startLatch.await();
        service.start();
        doneLatch.countDown();
    }).start();
}
startLatch.countDown();  // 发出开始信号
doneLatch.await();        // 等待所有服务启动完成

// CyclicBarrier:多线程互相等待
// 场景:多线程并行处理数据块后汇总
CyclicBarrier barrier = new CyclicBarrier(N, () -> {
    // barrierAction: 所有线程到达后执行
    executor.execute汇总();
});

for (int i = 0; i < N; i++) {
    final int id = i;
    new Thread(() -> {
        process(id);
        barrier.await();  // 等待其他线程
        continueWithResult();  // 所有线程到达后继续
    }).start();
}

【面试官心理】 这道题我能问到 P7 级别,是因为它涉及了 AQS 共享模式和多线程同步原语的设计哲学。能说清 CountDownLatch 和 CyclicBarrier 本质区别的候选人说明他理解了不同同步场景的需求差异。能解释"一方等待另一方"vs"多方互相等待"的候选人说明他有实际的工程设计能力。

1.4 追问升级

追问 1:countDown() 在 await() 之后调用会发生什么?

如果 countDown() 时已经有线程在 await(),则调用 countDown() 的线程立即返回,doReleaseShared() 唤醒所有等待线程。剩余的 countDown() 调用也正常执行(state 继续递减,但不产生额外唤醒)。

追问 2:CountDownLatch 可以用于超时场景吗?

可以。使用 await(long timeout, TimeUnit unit)awaitNanos(long nanosTimeout)

if (!latch.await(5, TimeUnit.SECONDS)) {
    // 超时:部分任务未完成
}

二、生产避坑 🟡

2.1 计数设置错误

// 错误:计数为 0
CountDownLatch latch = new CountDownLatch(0);
latch.await();  // 立即返回,因为 state 初始为 0

// 正确:计数值 = 子任务数量
CountDownLatch latch = new CountDownLatch(taskCount);

2.2 countDown 未被调用

如果某个子任务因异常未调用 countDown()await() 会永久阻塞。

解决:使用 try-finally 确保 countDown:

try {
    doWork();
} finally {
    latch.countDown();  // 即使异常也执行
}

三、CyclicBarrier 的额外特性 🟢

3.1 BrokenBarrier

如果某个线程在 await() 期间被中断,或 barrier 被 reset,所有正在等待的线程会抛出 BrokenBarrierException

// 检测 barrier 是否损坏
if (barrier.isBroken()) {
    // barrier 已损坏
}

3.2 CyclicBarrier vs Phaser

JDK 7 的 Phaser 是更灵活的屏障,支持动态注册参与方数量、多次同步点(比 CyclicBarrier 更强大):

Phaser phaser = new Phaser(3);  // 初始 3 个参与方
phaser.arriveAndAwaitAdvance();  // 等待其他线程到达
💡

面试加分点:能说出"CountDownLatch 的 state 使用 volatile int 存储,但 compareAndSetState 使用 CAS 保证递减的原子性",说明他理解了状态管理的细节。

⚠️

面试陷阱:被问到"CountDownLatch 能被重置吗",答"能"是错的。CountDownLatch 是不可重置的。如果需要可重置的倒计时,使用 Phaser 或在每次使用前创建新的 CountDownLatch。