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次
}

关键点解析:

  1. wait()使线程进入WAITING状态,释放CPU资源
  2. 必须配合循环使用,防止虚假唤醒(Spurious Wakeup)
  3. notify()唤醒线程,但需重新获取锁
  4. 中断处理:捕获InterruptedException后恢复中断状态

3.2. 现代替代方案

虽然wait()/notify()有效,但现代Java提供了更简洁的并发工具:

工具类 核心机制 适用场景
CountDownLatch await()阻塞,countDown()唤醒 一次性同步
CompletableFuture 异步回调/阻塞获取 异步任务编排
Lock + Condition await()/signal() 灵活锁控制
Semaphore 许可证机制 资源限流
CyclicBarrier 屏障同步 多线程阶段性同步

优势对比

  • CountDownLatch:简单粗暴的倒计时器,避免忙等待
  • CompletableFuture:天然异步,无需主动轮询
  • Lock/Condition:比synchronized更灵活的等待/通知机制

4. 总结

忙等待是多线程编程中的性能杀手,核心问题在于:

  • ❌ 持续消耗CPU资源
  • ❌ 降低系统响应性
  • ❌ 可能导致优先级反转

最佳实践

  1. 优先使用现代并发工具(CountDownLatch/CompletableFuture等)
  2. 传统场景下用wait()/notify()替代忙等待
  3. 始终在循环中调用wait()防止虚假唤醒
  4. 正确处理InterruptedException

完整代码示例见:GitHub仓库


原始标题:How to Avoid Busy-Waiting in Java | Baeldung