浅谈并发编程中三种常见的设计模式
很多兄弟一写多线程,脑子里全是 synchronized、Lock 和 Thread.sleep(),写出来的代码不仅性能拉胯,还时不时给你报个死锁。到了面试,面试官问一句:“如果让你手写一个 RPC 调用的同步等待,你怎么做?”瞬间就懵了。
其实,并发编程根本不需要死记硬背 API。所有的并发难题,底层都逃不出三大经典设计模式。今天,我们就把这些生涩的理论,降维成生活中最接地气的场景,并彻底拆解 JUC 里的两大核武神器(BlockingQueue 与 CountDownLatch),带你彻底打穿并发编程的“任督二脉”!
一、 同步模式之保护性暂停 (Guarded Suspension)
1. 是什么:餐厅点餐与“死等”的盖饭
在多线程世界里,有一种极其常见的一对一强关联场景:线程 A 必须等待线程 B 准备好某个特定的结果,才能继续往下走。
场景降维:你去餐厅点了一份青椒肉丝盖饭。付完钱拿到小票(共享对象)后,你不能去干别的,只能坐在大厅干等(线程阻塞)。直到后厨(另一个线程)把属于你的那份盖饭做好了,喊你的号,你才能端走吃饭。
在实战中,这就是 Future.get() 和所有 RPC 远程调用(如调用微信支付接口)的底层核心思想:发起请求 -> 线程挂起等待 -> 网络结果返回 -> 唤醒线程继续执行。
2. 怎么用:手写一个极简版 RPC 同步等待
我们用最原生的 wait/notify 来还原这个过程:
Java
public class GuardedSuspensionDemo {
// 共享的取餐小票(保护性对象)
static class GuardedObject {
private Object response;
// 获取结果(顾客等饭)
public synchronized Object get() {
// 【重点避坑】:必须用 while,绝不能用 if!
while (response == null) {
try {
System.out.println(Thread.currentThread().getName() + ":盖饭还没好,继续干等...");
wait(); // 释放锁,进入等待队列
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return response;
}
// 产生结果(后厨出餐)
public synchronized void complete(Object res) {
this.response = res;
System.out.println(Thread.currentThread().getName() + ":盖饭做好了!叮!");
notifyAll(); // 唤醒所有等待的线程
}
}
public static void main(String[] args) throws InterruptedException {
GuardedObject ticket = new GuardedObject();
new Thread(() -> {
Object meal = ticket.get();
System.out.println(Thread.currentThread().getName() + ":终于吃上了 " + meal);
}, "顾客").start();
Thread.sleep(1000); // 模拟做饭耗时
new Thread(() -> {
ticket.complete("青椒肉丝盖饭");
}, "后厨").start();
}
}
3. 重点细节:为什么必须用 while 而不是 if?
原因是:虚假唤醒 (Spurious Wakeup)。
如果用 if (response == null),当线程被 notifyAll() 唤醒时,它会直接跳出 if 块往下执行。但此时唤醒你的可能是操作系统的底层信号,或者盖饭其实是被别人拿走了。用 while 可以在唤醒后再次检查条件,如果还是没饭,乖乖回去继续 wait()。
二、 异步模式之生产者消费者 (Producer-Consumer)
1. 是什么:快递流水线与“完美解耦”
如果说保护性暂停是“一对一”的死等,那生产者消费者就是多对多的彻底解耦。这是并发编程中最伟大的模式,没有之一。
场景降维:想象一条双 11 的快递流水线。卸货工人(生产者)疯狂往传送带(中间队列)上扔包裹,扔完就走。分拣工人(消费者)站在另一头按自己的节奏拿包裹。传送带满了,卸货工就歇会儿;传送带空了,分拣工就坐着玩手机。
实战:大到分布式的 Kafka、RocketMQ(削峰填谷),小到单机版线程池(调用者提交任务,工作线程执行任务),全都是这个模式的演化。
2. 神器剖析:BlockingQueue 到底聪明在哪?
在 JDK 中,千万别自己去手写 wait/notify 搞队列,Doug Lea 大神早就为你准备好了 BlockingQueue(阻塞队列)。
-
大白话直觉:它就是一条自带红绿灯的智能传送带。
-
底层原理:它内部封装了锁(如
ReentrantLock)和条件变量(Condition)。-
当你调用
put()往里扔数据时,如果队列满了,它会自动把生产者线程挂起(亮红灯),直到消费者拿走一个数据,它才唤醒生产者继续扔。 -
当你调用
take()拿数据时,如果队列空了,它会自动把消费者挂起,直到生产者扔进来新数据。
-
-
核心意义:开发者完全不需要关心锁的释放与获取,也不用管什么时候该
wait什么时候该notify,直接傻瓜式调用即可!
3. 怎么用:一行代码搞定解耦
Java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ProducerConsumerDemo {
public static void main(String[] args) {
// 容量为 3 的智能传送带 (底层是数组实现的 ArrayBlockingQueue)
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
// 卸货工人(生产者)
new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
String parcel = "包裹-" + i;
queue.put(parcel); // 队列满了?自动阻塞等红灯!
System.out.println("📦 生产了:" + parcel);
}
} catch (InterruptedException e) {}
}, "生产者").start();
// 分拣工人(消费者)
new Thread(() -> {
try {
while (true) {
Thread.sleep(1000); // 消费者动作很慢
String parcel = queue.take(); // 队列空了?自动阻塞等红灯!
System.out.println("🚚 消费了:" + parcel);
}
} catch (InterruptedException e) {}
}, "消费者").start();
}
}
三、 同步模式之顺序控制 (Sequential Control)
1. 是什么:4x100 米接力赛的死规矩
多线程天生是交替随机执行的,谁先抢到 CPU 谁先跑。但有些业务必须讲规矩,强行打破这种随机性。
场景降维:田径场上的 4x100 米接力赛。第二棒选手(线程 2)就算体力再好,也绝对不能提前跑。他必须死死盯着第一棒(线程 1),只有交接棒塞到他手里的那一刻,他才能激活狂奔。
实战:系统启动时的依赖加载。比如:线程 B 负责启动定时任务,但它必须等待线程 A 把数据库连接池初始化完毕后才能动弹。
2. 神器剖析:CountDownLatch 的“倒计时”魔法
早期大家喜欢用 boolean flag 配合死循环来做,不仅浪费 CPU 还容易写出 Bug。JUC 包提供了一个极其优雅的组件:CountDownLatch(倒计时锁)。
-
大白话直觉:想象一扇被施了魔法的大门,门上挂着一把带数字的锁(比如数字是 3)。你想出门(
await()),就必须站在门后等。只有当外面的人完成任务并按下 3 次减号按钮(countDown()),锁上的数字归零,大门才会轰然打开,你才能冲出去。 -
底层原理:基于 AQS(AbstractQueuedSynchronizer)实现。它的
state变量就是那个倒计时数字。调用countDown()就是用 CAS 安全地把state减 1;调用await()的线程会被扔进 AQS 的同步队列里阻塞,直到state == 0时,AQS 会自动唤醒队列里的所有挂起线程。
3. 怎么用:优雅实现交接棒
Java
import java.util.concurrent.CountDownLatch;
public class SequentialControlDemo {
public static void main(String[] args) {
// 门上的密码锁,初始值为 1(只需要等一个前置任务)
CountDownLatch baton = new CountDownLatch(1);
// 线程 B:第二棒
new Thread(() -> {
try {
System.out.println("🏃 第二棒:在起跑线等待接力棒...");
baton.await(); // 死死堵在门后,直到数字归零
System.out.println("🏃 第二棒:拿到棒子,疯狂冲刺!");
} catch (InterruptedException e) {}
}).start();
// 线程 A:第一棒
new Thread(() -> {
try {
System.out.println("💨 第一棒:发令枪响,开始奔跑...");
Thread.sleep(1000); // 跑了 1 秒
System.out.println("💨 第一棒:交接棒!");
baton.countDown(); // 按下减号,数字 1 变成 0,大门敞开!
} catch (InterruptedException e) {}
}).start();
}
}
4. 重点细节:为什么不用 Thread.sleep() 硬等?
很多新手遇到顺序依赖,直接在线程 B 里写个 Thread.sleep(2000),假定线程 A 两秒内肯定能跑完。这种代码极其脆弱!如果 A 跑了 3 秒,B 就会拿到脏数据直接空指针报错;如果 A 跑了 0.1 秒,B 还要傻傻多等 1.9 秒,白白浪费 CPU。真正的顺序控制,必须基于如 CountDownLatch 这般精确的信号传递。
总结
无论怎么给你绕复杂的业务场景,只要你能把问题套进下面这三套模板里,并发编程对你来说就不再是玄学:
| 模式名称 | 核心理念 | 生活隐喻 | JUC 核武神器 | 避坑指南 |
| 保护性暂停 | 等待特定结果 | 餐厅点饭死等号 | wait() / notify() |
必须配合 while 循环使用,防虚假唤醒。 |
| 生产者消费者 | 解耦、削峰填谷 | 快递传送带 | BlockingQueue |
严禁使用废弃的 suspend/resume,会导致死锁。 |
| 顺序控制 | 强行规定执行先后 | 接力跑交接棒 | CountDownLatch |
严禁使用 Thread.sleep() 靠猜时间来硬等。 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)