Java多线程编程1-CountDownLatch

CountDownLatch介绍

CountDownLatch 是多线程控制的一种工具,它被称为 门阀、 计数器或者 闭锁。它的核心思想是允许一个或多个线程等待其他线程完成操作。这个工具经常用来用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。

CountDownLatch 能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。它相当于是一个计数器,这个计数器的初始值就是线程的数量(n),每当一个任务完成后,计数器的值就会减一,当计数器的值为 0 时,表示所有的线程都已经任务了,在 CountDownLatch 上 await 的线程就会被唤醒,然后在 CountDownLatch 上等待的线程就可以恢复执行接下来的任务。

特性

  1. 一次性使用:CountDownLatch 的计数器不能被重置或重复使用。
  2. 线程安全:所有的方法都是线程安全的。
  3. 只递减,不递增:计数器只能通过调用 countDown() 减少,不能增加。

有一点要说明的是CountDownLatch初始化后计数器值递减到0的时候,不能再复原的,这一点区别于Semaphore,Semaphore是可以通过release操作恢复信号量的。(后面会讲)

应用场景

  1. 并发任务协调:多个线程并行执行,主线程等待所有子线程完成后再执行后续操作。
  2. 服务启动检查:一个服务需要依赖多个子组件或子服务的启动。当一个服务启动时,同时会加载很多组件和服务,这时候主线程会等待组件和服务的加载。当所有的组件和服务都加载完毕后,主线程和其他线程在一起完成某个任务。
  3. 并行计算结果合并:将任务分割为多个部分并行计算,等待所有部分完成后合并结果。

使用方法

CountDownLatch 的使用方法非常简单,主要调用两个方法即可: countDown()await()

1. void countDown()

功能:将计数器减 1(线程任务完成后调用)。
使用规范

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> {
System.out.println("Thread 1 completed");
latch.countDown();
}).start();

new Thread(() -> {
System.out.println("Thread 2 completed");
latch.countDown();
}).start();

new Thread(() -> {
System.out.println("Thread 3 completed");
latch.countDown();
}).start();

latch.await();
System.out.println("All threads completed");

await 方法

CountDownLatch 中的 await 方法有两种,一种是不带任何参数的 await(),一种是可以等待一段时间的await(long timeout, TimeUnit unit)。下面我们先来看一下 await() 方法。

2. void await()

功能:使调用线程进入等待状态,直到计数器减到 0 或被中断。
使用规范

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
try {
System.out.println("Thread started");
Thread.sleep(2000); // 模拟任务
latch.countDown();
System.out.println("Thread finished");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();

latch.await(); // 主线程等待
System.out.println("Main thread resumed");

3. boolean await(long timeout, TimeUnit unit)

功能:与 await() 类似,但允许设置超时时间。如果计数器未归零,线程会在超时后继续执行。
使用规范

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
try {
Thread.sleep(5000); // 模拟长时间任务
latch.countDown();
System.out.println("Thread completed");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();

if (latch.await(3, TimeUnit.SECONDS)) {
System.out.println("Completed within timeout");
} else {
System.out.println("Timeout waiting for thread");
}

4. long getCount()

功能:返回当前计数器的值。
使用技巧

示例

1
2
3
4
CountDownLatch latch = new CountDownLatch(5);
System.out.println("Initial count: " + latch.getCount());
latch.countDown();
System.out.println("Current count: " + latch.getCount());

5. String toString()

功能:返回包含计数器值的字符串。
用途:可用于日志记录或调试。

示例

1
2
CountDownLatch latch = new CountDownLatch(3);
System.out.println(latch.toString()); // 输出类似 CountDownLatch{count = 3}

使用技巧和注意事项

  1. 一次性使用
    CountDownLatch 是一次性工具,使用后计数器不可重置。如果需要重复使用,请考虑使用 CyclicBarrier

  2. 避免死锁
    确保所有 countDown() 都会被调用,否则 await() 永远不会被唤醒,导致死锁。

  3. **多线程调用 countDown()**:
    多个线程可以同时调用 countDown(),无需担心线程安全问题。

  4. 适配线程池
    使用线程池时,可以将任务分配给线程池的线程执行,减少线程开销。

  5. 结合其他同步工具
    CountDownLatch 可以与 ExecutorServiceSemaphore 结合使用,以实现更复杂的线程控制逻辑。

常见对比

Q1: CountDownLatch 和 CyclicBarrier 的区别?


Q2: CountDownLatch 和 Semaphore 的区别?

应用

调用3个线程实现轮流打印数字从1至100:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import java.util.concurrent.CountDownLatch;

/**
* 使用 Implements Runnable 实现三个线程循环顺序打印 1-100,结合 CountDownLatch
*/
public class CountDownLatchStudy2 {

private static int number = 1; // 共享变量,用于记录当前打印的数字
private static final int total = 100; // 总数字范围

public static void main(String[] args) {
printNumbers();
}

static void printNumbers() {
CountDownLatch latch = new CountDownLatch(3); // 用于线程结束的计数器

// 创建三个线程,分别打印序列中的数字
Thread t1 = new Thread(new PrintTask(1, latch), "Thread-1");
Thread t2 = new Thread(new PrintTask(2, latch), "Thread-2");
Thread t3 = new Thread(new PrintTask(0, latch), "Thread-3");

t1.start();
t2.start();
t3.start();

try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

static class PrintTask implements Runnable{
private final int threadId;
private final CountDownLatch latch;

public PrintTask(int threadId, CountDownLatch latch){
this.threadId = threadId;
this.latch = latch;
}

@Override
public void run(){
while (number <= total) {
synchronized (CountDownLatchStudy2.class) {
// 判断是否轮到当前线程打印 加上判断 number <= total 防止多线程情况下超过100
if (number % 3 == threadId && number <= total) {
System.out.println(Thread.currentThread().getName() + " " + number);
number++;
}
}
}
latch.countDown();
}

}
}

参考

https://github.com/crisxuan/bestJavaer
https://www.cnblogs.com/cxuanBlog/p/14166322.html

https://www.cnblogs.com/Andya/p/12925634.html

About this Post

This post is written by Rui Xu, licensed under CC BY-NC 4.0.

#Java