本文涵盖 CountDownLatch 原理、使用场景、Java 示例、泳道图、可视化动画模拟,一篇文章看完就不会再踩坑。
标签:Java, CountDownLatch, 并发编程, 多线程同步, 生产者消费者, 并发可视化, HTML动画, 可视化编程
1. 什么是 CountDownLatch?
CountDownLatch
是 Java 并发包(java.util.concurrent
)中的一个同步工具类,它允许一个或多个线程等待,直到一组操作完成。
- 核心思想:维护一个倒计数(Count),线程可以调用
countDown()
来递减计数。当计数到达 0 时,等待的线程会被唤醒。 -
使用场景:
- 主线程等待多个子线程执行完毕(类似批处理任务)
- 多个线程等待某个条件满足后再一起执行(类似起跑枪)
- 控制多个阶段的同步执行
2. CountDownLatch 关键 API
CountDownLatch latch = new CountDownLatch(3); // 初始计数为3
latch.countDown(); // 计数减1
latch.await(); // 等待直到计数为0
CountDownLatch(int count)
:初始化计数器countDown()
:执行完一个任务后调用,计数器减1await()
:阻塞当前线程直到计数器为0
3. 实战场景:猴子抢香蕉 🍌
我们用一个有趣的例子来说明:
有一个容量为 20
的篮子,每次随机放 3~5
根香蕉,有三只猴子,每次取 1~3
根香蕉,取完后进入下一轮。
3.1 Java 实现
import java.util.Random;
import java.util.concurrent.CountDownLatch;
public class MonkeyBanana {
static int bananas = 0;
static final int MAX_CAPACITY = 20;
static final Random rand = new Random();
public static void main(String[] args) throws InterruptedException {
while (true) {
// 放香蕉
int put = rand.nextInt(3) + 3; // 3~5
bananas = Math.min(MAX_CAPACITY, bananas + put);
System.out.println("🍌 生产者放了 " + put + " 根香蕉,当前有 " + bananas + " 根");
// 三只猴子取香蕉
CountDownLatch latch = new CountDownLatch(3);
for (int i = 1; i <= 3; i++) {
final int id = i;
new Thread(() -> {
int take = rand.nextInt(3) + 1; // 1~3
synchronized (MonkeyBanana.class) {
int actual = Math.min(bananas, take);
bananas -= actual;
System.out.println("🐒 猴子" + id + " 拿了 " + actual + " 根香蕉,剩余 " + bananas + " 根");
}
latch.countDown();
}).start();
}
// 等猴子都取完
latch.await();
System.out.println("✅ 本轮结束,剩余香蕉:" + bananas + "\n");
Thread.sleep(1000); // 下一轮
}
}
}
运行结果示例:
🍌 生产者放了 4 根香蕉,当前有 4 根
🐒 猴子1 拿了 2 根香蕉,剩余 2 根
🐒 猴子3 拿了 1 根香蕉,剩余 1 根
🐒 猴子2 拿了 1 根香蕉,剩余 0 根
✅ 本轮结束,剩余香蕉:0
3.2 泳道图版本(Mermaid)
sequenceDiagram
participant Producer as 生产者
participant Monkey1 as 猴子1
participant Monkey2 as 猴子2
participant Monkey3 as 猴子3
participant Basket as 篮子
Producer->>Basket: 放3~5根香蕉
par 猴子1取香蕉
Monkey1->>Basket: 取1~3根
and 猴子2取香蕉
Monkey2->>Basket: 取1~3根
and 猴子3取香蕉
Monkey3->>Basket: 取1~3根
end
Basket-->>Producer: CountDownLatch归零
Producer->>Basket: 进入下一轮
3.3 时间线版本
t0: 生产者放香蕉
t1: 猴子1 开始取香蕉
t2: 猴子2 开始取香蕉
t3: 猴子3 开始取香蕉
t4: 所有猴子取完,CountDownLatch = 0
t5: 进入下一轮
3.4 浏览器可视化动画版本
我们可以用 HTML + JavaScript 做一个动态可视化模拟,保存为 banana_game.html
,用浏览器打开即可运行:
点击展开可视化动画代码
```html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>香蕉篮子 - CountDownLatch 模拟</title>
<style>
body { font-family: Arial, sans-serif; background: #f9f6ee; text-align: center; }
h1 { color: #f4a261; }
#basket { margin: 20px auto; width: 300px; height: 100px; background: #d4a373; border-radius: 15px; padding: 10px; }
.banana { width: 20px; height: 50px; background: yellow; border-radius: 10px 10px 0 0; margin: 2px; display: inline-block; }
.monkey { display: inline-block; width: 100px; margin: 20px; font-size: 20px; }
.log { margin-top: 20px; height: 150px; overflow-y: auto; border: 1px solid #ccc; padding: 5px; background: white; }
</style>
</head>
<body>
<h1>🍌 香蕉篮子 - CountDownLatch 动画模拟</h1>
<div id="basket"></div>
<div>
<div class="monkey">🐒 猴子1</div>
<div class="monkey">🐒 猴子2</div>
<div class="monkey">🐒 猴子3</div>
</div>
<div class="log" id="log"></div>
<script>
let bananas = 0, maxCapacity = 20;
const logBox = document.getElementById("log");
function log(msg){ logBox.innerHTML += msg + "<br>"; logBox.scrollTop = logBox.scrollHeight; }
function renderBasket(){ const b = document.getElementById("basket"); b.innerHTML=""; for(let i=0;i<bananas;i++){ let banana=document.createElement("div"); banana.className="banana"; b.appendChild(banana);}}
async function run(){
while(true){
let put=Math.floor(Math.random()*3)+3;
bananas=Math.min(maxCapacity,bananas+put);
log(`🍌 生产者放了 ${put} 根香蕉,当前有 ${bananas} 根`); renderBasket();
let promises=[];
for(let i=1;i<=3;i++){
promises.push(new Promise(r=>{
setTimeout(()=>{
let take=Math.floor(Math.random()*3)+1;
let actual=Math.min(bananas,take);
bananas-=actual;
log(`🐒 猴子${i} 拿了 ${actual} 根香蕉,剩余 ${bananas} 根`);
renderBasket();
r();
},Math.random()*2000+500);
}));
}
await Promise.all(promises);
log(`✅ 本轮结束,篮子还剩 ${bananas} 根香蕉\n`);
await new Promise(r=>setTimeout(r,1000));
}
}
run();
</script>
</body>
</html>
```
4. 总结
CountDownLatch
适用于一次性倒计数的线程同步场景- 用于生产者消费者模式时,可以让生产者等待多个消费者完成
-
我们的猴子抢香蕉例子展示了:
- Java 多线程实现
- 泳道图(Mermaid)
- 时间线
- 浏览器动画模拟
一句话记忆:CountDownLatch
就像“闸门”,关上后大家一起干活,等所有人完成后闸门才会打开。