【多线程】门栓/闭锁(Latch/CountDownLatch)
【多线程】门栓/闭锁(Latch/CountDownLatch)
本文来自于我关于多线程的系列文章。欢迎阅读、点评与交流
1.【多线程】互斥锁(Mutex)是什么?
2.【多线程】临界区(Critical Section)是什么?
3.【多线程】计算机领域中的各种锁
4.【多线程】信号量(Semaphore)是什么?
5.【多线程】信号量(Semaphore)常见的应用场景
6.【多线程】条件变量(Condition Variable)是什么?
7.【多线程】监视器(Monitor)是什么?
8.【多线程】什么是原子操作(Atomic Operation)?
9.【多线程】竞态条件(race condition)是什么?
10.【多线程】无锁数据结构(Lock-Free Data Structures)是什么?
11.【多线程】线程休眠(Thread Sleep)的底层实现
12.【多线程】多线程的底层实现
13.【多线程】读写锁(Read-Write Lock)是什么?
14.【多线程】死锁(deadlock)
15.【多线程】线程池(Thread Pool)
16.【多线程】忙等待/自旋(Busy Waiting/Spinning)
17.【多线程】阻塞等待(Blocking Wait)(以Java为例)
18.【多线程】阻塞等待(Blocking Wait)(以C++为例)
19.【多线程】屏障(Barrier)
20.【多线程】闭锁(Latch/CountDownLatch)
21.【多线程硬件机制】总线锁(Bus Lock)是什么?
22.【多线程硬件机制】缓存锁(Cache Lock)是什么?
1. 核心概念
门栓/闭锁(Latch/CountDownLatch)是一种同步工具类,它允许一个或多个线程等待,直到在其他线程中执行的一系列操作完成。(相比起闭锁(CountDownLatch),我觉得门栓(Latch)翻译更好,更形象)
你可以把它想象成一扇门闩(Latch):
- 在门闩关闭时,任何线程都无法通过。
- 当门闩打开时,所有线程都可以通过。
- 门闩一旦打开,就永远保持打开状态,不能再关闭。
在并发编程中,闭锁用于确保某些活动直到其他活动完成后才继续执行。
2. 核心工作原理
闭锁内部维护着一个计数器。这个计数器的初始值在创建时被设定,代表需要等待完成的“事件”数量。
- 计数递减:当一个线程完成了一个事件,它就调用
countDown()
方法,这会使计数器减1。 - 等待:其他需要等待所有这些事件完成的线程(例如主线程),会调用
await()
方法。 - 阻塞与释放:
- 调用
await()
的线程会被阻塞(即暂停执行),直到计数器的值变为 0。 - 当计数器减到 0 时,所有在
await()
上等待的线程都会被唤醒并继续执行。
- 调用
关键特性:
- 一次性:闭锁的状态是不可逆的。一旦计数器到达 0,它就永远保持在 0 的状态。所有后续调用
await()
的线程都会立即通过,不会再被阻塞。 - 协作:它实现了线程之间的协作,而不是互斥(像锁那样)。
3. 主要用途和场景
闭锁在以下场景中非常有用:
-
确保服务在依赖项就绪后才开始:
例如,一个主服务需要等待其他几个基础服务(如数据库连接池、网络服务、配置文件加载等)全部初始化完成后才能启动。每个基础服务启动完毕后就调用countDown()
,主服务在await()
处等待。 -
等待所有线程完成初始化任务:
在游戏服务器中,可能需要等待所有玩家都加载完地图和数据后再开始游戏。 -
最大并发测试:
创建一个初始值为 N 的闭锁。启动 N 个线程,每个线程都在开始执行测试任务前调用await()
。主线程在启动所有线程后调用countDown()
,由于初始值为1,所以这会导致所有等待的线程在同一时刻开始执行,从而模拟出最大的并发压力。 -
计算任务的分解与汇总:
将一个大型计算任务分解为多个子任务,每个子任务完成后调用countDown()
。一个汇总线程等待所有子任务完成后,再进行结果汇总。
4. 代码示例(Java)
在 Java 中,最常用的闭锁实现是 java.util.concurrent.CountDownLatch
。
场景:主线程需要等待三个工作线程都完成各自的任务后,才能继续执行。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;public class CountDownLatchDemo {public static void main(String[] args) throws InterruptedException {// 1. 创建一个初始计数器为 3 的闭锁CountDownLatch latch = new CountDownLatch(3);// 2. 创建并启动三个工作线程for (int i = 1; i <= 3; i++) {new Thread(new Worker(latch), "Worker-" + i).start();}System.out.println("主线程正在等待所有工作线程完成...");// 3. 主线程在此等待,直到计数器变为0latch.await();// 4. 所有工作线程都已完成,主线程继续执行System.out.println("所有工作线程都已完成!主线程继续执行。");}static class Worker implements Runnable {private final CountDownLatch latch;public Worker(CountDownLatch latch) {this.latch = latch;}@Overridepublic void run() {try {// 模拟工作线程正在执行任务System.out.println(Thread.currentThread().getName() + " 正在执行任务...");TimeUnit.SECONDS.sleep((long) (Math.random() * 3 + 1)); // 模拟耗时System.out.println(Thread.currentThread().getName() + " 任务完成!");} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {// 5. 任务完成后,调用 countDown() 将计数器减1// 这个调用必须放在 finally 中,确保无论如何都会执行latch.countDown();}}}
}
可能的输出:
主线程正在等待所有工作线程完成...
Worker-1 正在执行任务...
Worker-2 正在执行任务...
Worker-3 正在执行任务...
Worker-1 任务完成!
Worker-3 任务完成!
Worker-2 任务完成!
所有工作线程都已完成!主线程继续执行。
5. 与其他同步工具的比较
-
与
CyclicBarrier
比较:- 闭锁:用于等待事件,是一次性的。一个线程
countDown()
,另一个线程await()
。 - 循环屏障:用于线程彼此等待,是可重用的。所有线程都必须在屏障处调用
await()
才能继续。
- 闭锁:用于等待事件,是一次性的。一个线程
-
与
Future
/CompletableFuture
比较:Future
更侧重于表示一个异步计算的结果。- 闭锁更侧重于一种**“就绪信号”**,它本身不携带计算结果,只关心“完成”这个状态。
总结
闭锁是一个简单而强大的线程同步工具,它通过一个计数器来实现**“等待-通知”的机制。其核心思想是让一个或多个线程等待,直到一组操作在其他线程中执行完成。它的一次性**特性使其特别适用于那种“万事俱备,只欠东风”的并发场景。