原文来自于:zha-ge.cn/java/94
多线程乱成一锅粥?教你把线程按顺序乖乖排队!
不瞒你说,搞 Java 多线程这么多年,实际上------每次项目一但挨上"顺序执行线程"这活儿,我心里还是要打个突。明明大家都是线程,凭啥你就非要先我一步,把那个开关拧掉?
我先来点题吧:今天聊聊"多个线程如何按顺序执行"这档事,顺带聊点自己曾经踩过的那些坑,最后教你几招安安稳稳排个顺序。
线程乱炖的第一现场
事情还得从半年前一个改接口加日志的需求说起------
日志要打印A-B-C用户行为,每一步是不同的Service,老板要求:必须严格按A-B-C顺序输出!
"啊这,不就是'顺序执行'嘛!"我心里一乐,随手写了点线程池丢进去,各线程代码长这样:
java
new Thread(() -> {
// 处理A
System.out.println("A");
}).start();
new Thread(() -> {
// 处理B
System.out.println("B");
}).start();
// C同理......
执行结果嘛,你猜怎么着:
有时候是A-B-C,有时候B-A-C,偶尔来了个C-A-B...... 日志直接变成烩面,不知道到底是哪家用户干了啥。
"只要用 synchronized,就能排队"吗?
当时一拍大腿:同步锁大法好啊!一人写个synchronized
,大家顺序走。
但你细品,锁只是让两个线程不能同时进来,并没有保证哪个线程先、哪个线程后!
- 线程A抢占锁,输出A
- 同一时刻线程B、C虎视眈眈
- 谁抢到锁,是个天知道!
后来搞个"同步方法队列",或者让主线程sleep等待......诡异的并发问题还是层出不穷。
踩坑瞬间
说到这,不得不表演一波"踩坑翻车现场"了------
-
靠
sleep
控制顺序? 想当然地开 A 线程、sleep 100 毫秒、再开 B 线程------结果运行环境一变,延迟一乱,顺序直接扑街。 -
自定义 flag 变量? 线程疯狂 while(flag) 死等前面线程,结果无数 CPU 吃灰,服务器呜呼哀哉。
-
锁里套锁,信号量自杀? 几十个线程加锁、wait、notify,一不小心忘 notify,线程全卡死,运维@你"今天是不是又写挂了?"。
踩坑总结一句话:并发顺序,想糊弄,一定要翻车!
一招定江山:用信号量"排号进场"
后来我悟了------线程排队这种事,得玩点信号同步 的花样,不能靠碰运气。 最适配的还是CountDownLatch
、CyclicBarrier
、Semaphore
,尤其适合"第N步必须等前面干完再上"。
比如用CountDownLatch
实现ABC顺序:
java
CountDownLatch latchAB = new CountDownLatch(1);
CountDownLatch latchBC = new CountDownLatch(1);
new Thread(() -> {
System.out.println("A");
latchAB.countDown(); // 放行B
}).start();
new Thread(() -> {
latchAB.await();
System.out.println("B");
latchBC.countDown(); // 放行C
}).start();
new Thread(() -> {
latchBC.await();
System.out.println("C");
}).start();
注意:
- A线程干完,放行B线程(latchAB.countDown())
- B线程等A干完,才执行,再放行C(latchBC.countDown())
- 线程顺序如铁门槛,谁也插不队!
理论上还可以用Semaphore
、ReentrantLock+Condition
,但CountDownLatch
最容易上手。
经验启示
手法对比清单(文字版)
很多人一上来就问:"那线程顺序到底该怎么排?"我自己踩过不少坑,总结下来,大致有这么几种手法:
首先是最不推荐的 sleep 或 while 循环。 这种写法看起来像是用延时或者死等来控制顺序,实则完全靠运气。CPU 一旦调度不一样、网络环境一乱,顺序直接崩盘,而且 while 死等还会疯狂消耗 CPU。可以说是"一星警告",能不用就别碰。
接着是 synchronized。 synchronized 本质上是个互斥锁,它只能保证线程不会同时执行某一段代码,但没法保证顺序。换句话说,它只能让大家排队进门,却管不住谁先谁后。这个方法勉强能忍,但不是解决顺序问题的正道。
然后是 CountDownLatch。 这是我最常用、也是最推荐的方案。它适合那种明确的前后依赖,比如 A 干完才能轮到 B,B 干完才能轮到 C。一次性的顺序执行,用 CountDownLatch 简单高效。
再高级一点的是 CyclicBarrier。 它的特点是"可重复使用",就像一群人跑步,每圈跑完都要等大家集合齐了才能开下一轮。非常适合阶段性同步任务,不过场景要合适才行。
还有 Semaphore。 这个就像发号牌,限制一次能进多少人。用在控制并发数的时候最合适,如果你设计得巧妙,也能实现"按号排队"的效果。
最后是 ReentrantLock 搭配 Condition。 这是高阶玩家的工具,灵活度极高,你可以精确控制哪类线程该被唤醒,顺序可以自己编排。缺点是写起来比前几种都复杂,适合需要强可控性的场景。
怎么排队
多线程按顺序执行,说白了就是排队。
乱来靠运气 = 必翻车。
科学用工具(CountDownLatch、Semaphore 等)= 顺序稳如老狗。
经验之谈:别怕麻烦,写对一次比调 Bug 十次省心。