在多线程并发编程中,如果线程的执行有前置条件。比如做一个任务,需要执行A、B、C、D四个步骤,A和B可以同时进行,但是C的执行需要等到A和B执行结束,D要等到C执行结束。这种情况你会怎么做?
在这种情况下,CountDownLatch就可以发挥它的作用了。
一、CountDownLatch简介
它允许一个线程等待多个线程执行完毕后再继续执行。
通俗来说,它就像一个计数器,主线程会等待直到计数器的值减到零,所有的工作线程执行完毕。
工作原理:
CountDownLatch 初始化时,计数器的值是一个正整数,表示需要等待的事件数量。
每当一个线程完成它的任务后,会调用 countDown() 方法来将计数器减一。
其他线程可以调用 await() 方法等待计数器变为零,直到计数器值为零时,所有等待的线程才会继续执行。
二、CountDownLatch 1.8 源码解读
1. 构造函数
public class CountDownLatch implements java.io.Serializable {
private final Sync sync;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
// 内部 Sync 类(用于处理计数器同步的核心实现)
static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1L;
Sync (int count) {
setState(count); // 设置初始计数器值
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1; // 计数器为0时,表示可以获取锁
}
protected boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
if (current == 0) {
return false;
}
int next = current - 1;
if (compareAndSetState(current, next)) {
return next == 0;
}
}
}
}
}
CountDownLatch(int count) 用于初始化计数器 count,并创建一个 Sync 对象作为同步工具。计数器的初始值就是我们传递给构造函数的参数。
内部类 Sync 继承了 AbstractQueuedSynchronizer(AQS)。
AQS 是一个用于构建同步工具类的框架,它通过管理锁的状态(如计数器值)来实现并发控制。
Sync 通过重写 AQS 的 tryAcquireShared 和 tryReleaseShared 方法来实现计数器的获取和释放。
2. await() 方法:让当前线程等待
await() 方法会让当前线程阻塞,直到计数器的值变为 0。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
功能:调用 await() 方法后,当前线程会被阻塞,直到计数器减到零或者被中断。acquireSharedInterruptibly(1) 会尝试以共享模式获取锁,并且支持响应中断。
底层实现:通过 AbstractQueuedSynchronizer 提供的 acquireSharedInterruptibly() 方法实现,线程被加入到阻塞队列中,直到计数器为零或者发生中断。
3. countDown() 方法:减少计数器
countDown() 方法会让计数器减一,当计数器减到 0 时,唤醒所有等待的线程。
public void countDown() {
sync.releaseShared(1);
}
功能:每次调用 countDown(),计数器减一。如果计数器减为零,会唤醒所有在 await() 中等待的线程。
底层实现:通过 AbstractQueuedSynchronizer 提供的 releaseShared() 方法实现,当 countDown() 被调用时,调用 tryReleaseShared() 减少计数器,并判断是否需要唤醒等待线程。
4. getCount() 方法:获取当前计数器的值
public long getCount() {
return sync.getState();
}
功能:返回当前计数器的值,用于检查还有多少个线程需要等待。
三、应用示例
模拟多个线程并行执行任务,假设我们有多个线程,每个线程执行一些任务,主线程会等待这些任务完成之后才继续执行。
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
// 创建一个 CountDownLatch,计数器为 3,表示需要等待 3 个线程完成任务
CountDownLatch latch = new CountDownLatch(3);
// 任务线程
Runnable task = () -> {
try {
// 模拟任务处理,假设每个任务处理需要 1 秒
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " finished.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // 每个线程完成任务后,调用 countDown()
}
};
// 启动 3 个线程来执行任务
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
// 主线程等待,直到计数器为 0,表示所有线程都已完成
latch.await(); // 等待所有任务完成
System.out.println("All threads finished. Main thread resumes.");
}
}
创建了一个 CountDownLatch 对象,计数器初始化为 3,表示主线程需要等待 3 个线程完成任务。
启动了 3 个线程,每个线程执行一个任务(模拟为 1 秒的睡眠),并在任务完成后调用 countDown() 来减少计数器。
主线程调用 await(),等待计数器值为 0 后才继续执行,最后输出“主线程恢复”。
运行结果:
Thread-0 finished.
Thread-1 finished.
Thread-2 finished.
All threads finished. Main thread resumes.
从上述示例中,我们看到,CountDownLatch 通过计数器来同步多个线程,主线程会等待计数器为 0 时才继续执行。
await():让当前线程阻塞,直到计数器为 0。
countDown():减少计数器的值。
四、CountDownLatch 的优缺点
CountDownLatch 是一个非常有用的并发控制工具,但它也有一些局限性。
优点:
简单易用:
CountDownLatch 提供了简单的 API,使用 await() 和 countDown() 方法即可实现线程间的同步,容易理解和使用。
线程同步:
它允许多个线程执行完任务后,主线程或其他线程才继续执行。比如在并发任务的最后阶段需要等待所有线程完成,可以非常方便地进行控制。
高效的线程等待:
CountDownLatch 使用的是基于 AbstractQueuedSynchronizer(AQS)的高效同步机制,线程通过 acquireShared() 和 releaseShared() 方法协调,避免了频繁的轮询,效率较高。
适用于一次性同步:
CountDownLatch 适用于需要一次性等待多个线程完成的场景。一旦计数器为 0,所有等待的线程会被唤醒,并且计数器无法重置,因此非常适合于这种单次等待。
缺点:
不能重用:
CountDownLatch 设计为只能使用一次。一旦计数器减到 0,CountDownLatch 就不再有效,无法重新启动计数器。因此,如果需要多次等待线程完成,CountDownLatch 不是合适的选择。对于这种情况,可以考虑使用 CyclicBarrier 或其他同步工具。
主线程阻塞:
如果主线程调用了 await(),它会被阻塞直到计数器减为 0。如果某些工作线程的任务执行时间较长,主线程可能会长时间阻塞,造成程序响应延迟或效率降低。
不支持中断后重试:
CountDownLatch 在 await() 阻塞时,如果线程被中断,它会抛出 InterruptedException,此时不能恢复继续等待。如果线程中断后需要重新尝试等待,CountDownLatch 不支持这种行为。这个问题在需要重试机制时需要额外的处理。
无法控制特定线程的等待:
CountDownLatch 是全局的,它让所有调用 await() 的线程一起阻塞并等待。若需要更细粒度的控制,如让某些线程等待其他特定线程完成任务,CountDownLatch 并不适合,可能需要使用 Semaphore 或其他工具。
五、最后总结
CountDownLatch 是一种简单而高效的工具,适用于许多并发编程场景。它的设计理念就是让一个或多个线程等待一组线程的执行完成,通常用于初始化或并行任务结束的同步。不过,它也有一些局限性,比如不能重用、阻塞主线程等。根据不同的需求,我们可以选择最合适的工具来解决问题。
领取专属 10元无门槛券
私享最新 技术干货