1. 写在前面
CountDownLatch 是 Java 中的一个同步辅助类,位于 java.util.concurrent 包中。它允许一个或多个线程等待直到一组操作完成。CountDownLatch 的主要用途是在某些线程需要等待其他线程完成某些任务之后才能继续执行的场景。这篇文章我们一起来看下CountDownLatch的源代码,如下几个问题知道各位读者有没有思考过:
- CountDownLatch 与 CyclicBarrier 有什么区别?
- 使用 CountDownLatch 的场景有哪些?
- CountDownLatch 的内部工作原理是什么?
2. 从使用说起
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
private static final int NUMBER_OF_THREADS = 3;
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(NUMBER_OF_THREADS);
// 启动三个线程
for (int i = 0; i < NUMBER_OF_THREADS; i++) {
final int threadId = i + 1;
new Thread(() -> {
try {
// 模拟任务执行
System.out.println("Thread " + threadId + " is doing some work.");
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 任务完成,计数器减一
System.out.println("Thread " + threadId + " has finished its work.");
latch.countDown();
}
}).start();
}
// 主线程等待,直到所有子线程完成
latch.await();
System.out.println("All threads have finished. Main thread resumes.");
}
}
以上代码是一个使用 CountDownLatch 的简单示例。运行结果如下:
Thread 1 is doing some work.
Thread 3 is doing some work.
Thread 2 is doing some work.
Thread 1 has finished its work.
Thread 3 has finished its work.
Thread 2 has finished its work.
All threads have finished. Main thread resumes.
主要步骤如下:
- 创建 CountDownLatch
CountDownLatch latch = new CountDownLatch(NUMBER_OF_THREADS); 创建一个 CountDownLatch 实例,初始计数为 3。 - 启动多个线程
使用一个循环启动 3 个线程。每个线程会模拟一些工作(使用 Thread.sleep) - 计数器减一
每个线程在完成工作后调用 latch.countDown(),将计数器减一 - 主线程等待
主线程调用 latch.await(),等待所有子线程完成工作 - 所有线程完成后
当所有线程完成后,计数器变为零,主线程被唤醒,继续执行。
2.1 创建
创建CountDownLatch的源代码如下:
/**
* Constructs a {@code CountDownLatch} initialized with the given count.
*
* @param count the number of times {@link #countDown} must be invoked
* before threads can pass through {@link #await}
* @throws IllegalArgumentException if {@code count} is negative
*/
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
2.1.1 参数说明
- int count: 这是构造方法的参数,表示在调用 countDown() 方法之前,计数器的初始值。这个值决定了有多少个事件需要完成,才能使得等待的线程通过 await() 方法继续执行。
2.1.2 逻辑解析
- if (count < 0) throw new IllegalArgumentException(“count < 0”)
- 这一行代码确保传入的计数器值是非负的。如果传入的 count 小于 0,构造方法将抛出 IllegalArgumentException。这是为了防止逻辑错误,因为计数器的值不能为负数。
- this.sync = new Sync(count);
- 这里创建了一个名为 Sync 的内部类实例,并将 count 传递给它。Sync 类负责实现 CountDownLatch 的核心同步逻辑,包括计数器的管理和线程的等待与唤醒。
2.2 countDown() 做了什么?
源代码如下:
/**
* Decrements the count of the latch, releasing all waiting threads if
* the count reaches zero.
*
* <p>If the current count is greater than zero then it is decremented.
* If the new count is zero then all waiting threads are re-enabled for
* thread scheduling purposes.
*
* <p>If the current count equals zero then nothing happens.
*/
public void countDown() {
sync.releaseShared(1);
}
CountDownLatch 的 countDown() 方法用于将计数器的值减一,并在计数器达到零时释放所有等待的线程。
主要的方法逻辑包括:
- 计数器减一
当调用 countDown() 方法时,sync.releaseShared(1) 被执行。这表示请求将计数器减一。 - 释放等待线程
如果计数器的当前值大于零,则它会被减一。如果减到零,所有等待的线程将被重新激活,允许它们继续执行 - 无效调用
如果当前计数器的值已经是零,调用 countDown() 不会产生任何效果,计数器的值不会被改变,也不会释放任何线程。
3. CountDownLatch 与 CyclicBarrier 有什么区别?
CountDownLatch 和 CyclicBarrier 都是 Java 并发包中的同步工具,用于协调多个线程的执行,但它们在设计目的、使用场景和行为上有显著的区别。以下是通过源代码和概念来说明这两者的主要区别。
3.1 设计目的
- CountDownLatch
主要用于让一个或多个线程等待直到某些操作完成。它是一次性的,计数器在达到零后无法重置。 - CyclicBarrier
允许一组线程相互等待,直到所有线程都到达某个公共屏障点。它是可重用的,经过一次屏障后,可以重新使用
3.2 源代码分析
3.2.1 CountDownLatch
CountDownLatch 的核心部分在于其计数器的管理,计数器的初始值在构造时设置,并且通过 countDown() 方法进行递减。以下是 CountDownLatch 的相关源代码片段:
public class CountDownLatch {
private final Sync sync;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
public void countDown() {
sync.releaseShared(1);
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
static final class Sync extends AbstractQueuedSynchronizer {
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current - releases;
if (next < 0) return false;
if (compareAndSetState(current, next)) {
return next == 0;
}
}
}
}
}
CountDownLatch 的 tryReleaseShared 方法中,计数器在调用 countDown() 时递减,并且当计数器减到零时,所有等待的线程会被释放。
3.2.2 CyclicBarrier
CyclicBarrier 的实现允许线程在达到某个点后相互等待,内部维护一个计数器,表示当前有多少线程到达了屏障。以下是 CyclicBarrier 的相关源代码片段:
public class CyclicBarrier {
private final int parties;
private int count;
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();
public CyclicBarrier(int parties) {
if (parties <= 0) throw new IllegalArgumentException("parties must be greater than zero");
this.parties = parties;
this.count = parties;
}
public int await() throws InterruptedException {
lock.lock();
try {
int index = --count;
if (count == 0) {
count = parties; // Reset for next use
trip.signalAll(); // Release all waiting threads
return index;
}
while (count > 0) {
trip.await(); // Wait for other threads
}
return index;
} finally {
lock.unlock();
}
}
}
在 CyclicBarrier 中,await() 方法会减少计数器,并在计数器达到零时释放所有等待的线程。然后它会重置计数器,允许下一轮的使用。
3.3 使用场景
- CountDownLatch
适用于需要等待某些任务完成的场景。例如,主线程需要等待多个子线程完成初始化工作后再继续执行。 - CyclicBarrier
适用于需要在多个线程之间进行阶段性同步的场景。例如,多个线程在完成各自的任务后需要在某个点汇合,再一起继续执行下一个阶段的任务。
3.4 复用性
- CountDownLatch
一旦计数器降到零,CountDownLatch 就不能重用,必须重新创建一个新的实例。可以在多次使用后重置,允许线程在完成一轮后再次进行同步。 - CyclicBarrier
可以在多次使用后重置,允许线程在完成一轮后再次进行同步。
4. 使用 CountDownLatch 的场景有哪些?
4.1 等待多个线程完成任务
当主线程需要等待多个子线程完成各自的任务后再继续执行时,可以使用 CountDownLatch。例如,在一个程序中,主线程可能需要等待多个初始化线程完成初始化操作。
CountDownLatch latch = new CountDownLatch(3); // 等待3个线程
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 执行初始化操作
// ...
latch.countDown(); // 每个线程完成后调用countDown
}).start();
}
latch.await(); // 主线程等待
// 所有线程完成后继续执行
4.2 实现并发测试
在并发测试中,可能需要多个线程同时开始执行。这时可以使用 CountDownLatch 来控制线程的启动时机。
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch doneLatch = new CountDownLatch(5); // 等待5个线程完成
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
startLatch.await(); // 等待开始信号
// 执行任务
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
doneLatch.countDown(); // 任务完成
}
}).start();
}
// 释放所有线程
startLatch.countDown(); // 所有线程同时开始
doneLatch.await(); // 等待所有线程完成
4.3 分阶段任务执行
在某些情况下,程序可能需要分阶段执行多个任务,某个阶段的完成依赖于其他阶段的结果。可以使用 CountDownLatch 来同步这些阶段。
CountDownLatch phase1Latch = new CountDownLatch(2); // 等待2个线程完成第一阶段
new Thread(() -> {
// 执行第一阶段任务
phase1Latch.countDown(); // 完成后递减计数
}).start();
new Thread(() -> {
// 执行第一阶段任务
phase1Latch.countDown(); // 完成后递减计数
}).start();
phase1Latch.await(); // 等待第一阶段完成
// 执行第二阶段任务
4.4 资源准备
在某些应用中,可能会有多个线程需要在开始工作之前准备一些资源。可以使用 CountDownLatch 来确保所有资源准备好后再开始工作。
CountDownLatch resourceLatch = new CountDownLatch(3); // 等待3个资源准备
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 准备资源
resourceLatch.countDown(); // 每个线程完成资源准备后调用countDown
}).start();
}
resourceLatch.await(); // 等待所有资源准备好
// 开始使用资源
4.5 控制并发执行
在某些情况下,可以使用 CountDownLatch 来限制并发执行的线程数。例如,限制同时执行的线程数为 N。
CountDownLatch latch = new CountDownLatch(N); // N是并发线程数
for (int i = 0; i < totalTasks; i++) {
new Thread(() -> {
try {
latch.await(); // 等待开始信号
// 执行任务
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
if (i % N == 0) {
latch.countDown(); // 每N个任务释放一次
}
}
系列文章
7.jdk源码阅读之ConcurrentHashMap(上)