1. 介绍
在多线程编程中,忙等待(Busy-Waiting)是个需要警惕的陷阱。本文将深入探讨:
- ✅ 忙等待的本质及其危害
- ❌ 为什么忙等待会浪费CPU资源
- ✅ 高效替代方案:阻塞机制
面向有经验的开发者,我们直接切入核心问题,避免基础概念赘述。
2. 什么是忙等待?
忙等待是多线程系统中的常见反模式,本质是:线程在循环中持续检查某个条件,直到条件满足。这会导致线程陷入"空转"状态,持续消耗CPU资源却无实际产出。
看这个踩坑案例:
@Test
void givenWorkerThread_whenBusyWaiting_thenAssertExecutedMultipleTimes() {
AtomicBoolean taskDone = new AtomicBoolean(false);
long counter = 0;
Thread worker = new Thread(() -> {
simulateThreadWork(); // 模拟耗时任务
taskDone.set(true);
});
worker.start();
// 主线程忙等待
while (!taskDone.get()) {
counter++; // 空转计数器
}
logger.info("Counter: {}", counter);
assertNotEquals(1, counter); // 断言计数远大于1
}
执行结果触目惊心:
11:14:32.286 [main] INFO c.b.c.b.BusyWaitingUnitTest - Counter: 885019109
⚠️ 主线程空转了8.85亿次!这就是忙等待的典型危害——无意义的CPU资源浪费。
3. 如何避免忙等待?
核心思路:用阻塞机制替代忙等待。阻塞机制让线程主动暂停,直到被显式唤醒,彻底避免空转。
3.1. 传统方案:wait() 和 notify()
Java内置的wait()
和notify()
是解决忙等待的经典方案:
@Test
void givenWorkerThread_whenUsingWaitNotify_thenWaitEfficientlyOnce() {
AtomicBoolean taskDone = new AtomicBoolean(false);
final Object monitor = new Object(); // 监视器对象
long counter = 0;
Thread worker = new Thread(() -> {
simulateThreadWork();
synchronized (monitor) {
taskDone.set(true);
monitor.notify(); // 唤醒等待线程
}
});
worker.start();
synchronized (monitor) {
while (!taskDone.get()) { // 防止虚假唤醒
counter++;
try {
monitor.wait(); // 阻塞而非忙等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
fail("Test case failed due to thread interruption!");
}
}
}
assertEquals(1, counter); // 仅检查1次
}
关键点解析:
wait()
使线程进入WAITING状态,释放CPU资源- 必须配合循环使用,防止虚假唤醒(Spurious Wakeup)
notify()
唤醒线程,但需重新获取锁- 中断处理:捕获
InterruptedException
后恢复中断状态
3.2. 现代替代方案
虽然wait()/notify()
有效,但现代Java提供了更简洁的并发工具:
工具类 | 核心机制 | 适用场景 |
---|---|---|
CountDownLatch | await() 阻塞,countDown() 唤醒 |
一次性同步 |
CompletableFuture | 异步回调/阻塞获取 | 异步任务编排 |
Lock + Condition | await() /signal() |
灵活锁控制 |
Semaphore | 许可证机制 | 资源限流 |
CyclicBarrier | 屏障同步 | 多线程阶段性同步 |
✅ 优势对比:
- CountDownLatch:简单粗暴的倒计时器,避免忙等待
- CompletableFuture:天然异步,无需主动轮询
- Lock/Condition:比
synchronized
更灵活的等待/通知机制
4. 总结
忙等待是多线程编程中的性能杀手,核心问题在于:
- ❌ 持续消耗CPU资源
- ❌ 降低系统响应性
- ❌ 可能导致优先级反转
✅ 最佳实践:
- 优先使用现代并发工具(
CountDownLatch
/CompletableFuture
等) - 传统场景下用
wait()/notify()
替代忙等待 - 始终在循环中调用
wait()
防止虚假唤醒 - 正确处理
InterruptedException
完整代码示例见:GitHub仓库