Java案例怎么唤醒等待线程?

wen java案例 10

Java案例:如何唤醒等待线程?——从原理到实战的完整指南

目录导读

  1. 线程等待与唤醒的核心机制
  2. Object类的wait/notify/notifyAll方法详解
  3. 实战案例:生产者-消费者模型中的线程唤醒
  4. Condition接口与Lock的灵活唤醒
  5. 常见陷阱与最佳实践
  6. Q&A高频问题解答

线程等待与唤醒的核心机制

在Java多线程编程中,线程等待与唤醒是解决协同问题的关键,当某个线程需要等待特定条件(如队列为空、资源未就绪)时,它应该释放锁并进入等待状态;当条件满足时,其他线程需要通知它继续执行,Java提供了两种主要机制:

Java案例怎么唤醒等待线程?

  • Object类的wait/notify/notifyAll(基于synchronized)
  • Condition接口(基于ReentrantLock)

核心问题:一个线程调用了wait()后,如何被重新唤醒?答案是:必须由另一个线程在持有同一把锁的前提下调用notify()notifyAll()


Object类的wait/notify/notifyAll方法详解

1 方法签名与前提条件

public final void wait() throws InterruptedException
public final void notify()
public final void notifyAll()

前提条件:调用这些方法的线程必须持有该对象的监视器锁(即必须在synchronized块或方法内),否则会抛出IllegalMonitorStateException

2 执行流程

  1. 线程A调用obj.wait()
    • 释放对象锁
    • 进入WAITING状态,加入该对象的等待集(Wait Set)
  2. 线程B调用obj.notify()
    • 随机唤醒等待集中的一个线程(非公平)
    • 被唤醒的线程从WAITING转为BLOCKED状态,尝试重新竞争锁
  3. 线程C调用obj.notifyAll()

    唤醒等待集中的所有线程,它们同时竞争锁

3 伪代码演示

synchronized (sharedObj) {
    while (!condition) {
        sharedObj.wait();  // 释放锁,进入等待
    }
    // 执行条件满足后的逻辑
}

注意:必须使用while循环检查条件,防止虚假唤醒(Spurious Wakeup)。


实战案例:生产者-消费者模型中的线程唤醒

场景描述

一个仓库最多存储10件商品,当仓库满时,生产者线程等待;当仓库空时,消费者线程等待,双方通过notifyAll相互唤醒。

代码实现

class Warehouse {
    private final int capacity = 10;
    private final List<String> items = new ArrayList<>();
    public synchronized void produce(String item) throws InterruptedException {
        while (items.size() == capacity) {
            System.out.println("仓库已满,生产者等待...");
            wait();
        }
        items.add(item);
        System.out.println("生产了 " + item + ",库存:" + items.size());
        notifyAll(); // 唤醒可能等待的消费者
    }
    public synchronized String consume() throws InterruptedException {
        while (items.isEmpty()) {
            System.out.println("仓库为空,消费者等待...");
            wait();
        }
        String item = items.remove(0);
        System.out.println("消费了 " + item + ",库存:" + items.size());
        notifyAll(); // 唤醒可能等待的生产者
        return item;
    }
}

关键点

  • notifyAll()notify()更安全,避免“信号丢失”导致死等
  • 使用while循环而非if检查条件,防止虚假唤醒破坏数据一致性

Condition接口与Lock的灵活唤醒

1 为什么需要Condition?

synchronizedwait/notify只能有一个等待集,难以实现精准唤醒(例如只唤醒生产者或只唤醒消费者)。ReentrantLock配合Condition可以创建多个等待集。

2 使用示例:用两个Condition实现精准唤醒

class BetterWarehouse {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();  // 生产者等待条件
    private final Condition notEmpty = lock.newCondition(); // 消费者等待条件
    private int count = 0;
    private final int capacity = 10;
    public void produce() throws InterruptedException {
        lock.lock();
        try {
            while (count == capacity) {
                notFull.await(); // 生产者等待
            }
            count++;
            System.out.println("生产后库存:" + count);
            notEmpty.signal(); // 只唤醒一个消费者
        } finally {
            lock.unlock();
        }
    }
    public void consume() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await(); // 消费者等待
            }
            count--;
            System.out.println("消费后库存:" + count);
            notFull.signal(); // 只唤醒一个生产者
        } finally {
            lock.unlock();
        }
    }
}

优势

  • 避免“惊群效应”(所有线程被唤醒却只有少数能竞争到锁)
  • 代码语义更清晰:notFullnotEmpty分别控制两类线程

常见陷阱与最佳实践

1 虚假唤醒(Spurious Wakeup)

即使没有其他线程调用notify,等待的线程也可能被操作系统唤醒。必须用while循环重试条件

2 信号丢失(Lost Signal)

  • 错误用法:在if中调用wait,条件变化后未被检查
  • 解决方法:始终使用while(!condition)

3 死锁与饥饿

  • 死锁:线程A持有锁X等待条件Y,线程B持有锁Y等待条件X
  • 饥饿:优先级低的线程长期得不到锁,可尝试公平锁

4 性能优化

  • 优先使用ReentrantLock + Condition替代synchronized,尤其是需要超时等待、公平锁、多个条件时
  • 避免在循环中频繁调用wait,确保条件判断高效

Q&A高频问题解答

Q1:notify()notifyAll()应该选哪个?

A:优先选notifyAll(),虽然notify()性能稍好,但它随机唤醒一个线程,可能导致信号丢失(唤醒的线程不满足条件而继续等待),除非你能确定所有等待线程条件相同,否则用notifyAll更安全。

Q2:wait()为什么要放在同步块中?

A:确保时序正确性,如果不在同步块中,可能出现:

  • 线程A检查条件满足,但此时线程B修改了条件
  • 线程A调用wait,却可能永远收不到通知(通知已经发出)
    同步块保证了检查条件和调用wait的原子性。

Q3:Conditionawait()sleep()有什么区别?

A

  • await()释放锁并等待,允许其他线程获取锁;sleep()不释放锁
  • await()可以被中断;sleep()也可以被中断但会抛出InterruptedException
  • await()需要与ConditionLock配合使用

Q4:如何唤醒指定线程?

A:Java本身不提供直接唤醒指定线程的API,但可通过:

  1. 使用Conditionsignal()只会唤醒一个等待线程(无法指定具体哪个)
  2. 使用标志变量+while循环,让线程自行决定是否继续等待
  3. 使用SemaphoreCountDownLatch等并发工具实现“点到点”唤醒

Q5:如果wait()时线程被中断会怎样?

A:线程会从等待状态立即返回,并抛出InterruptedException,建议在异常处理中重置中断状态:Thread.currentThread().interrupt()


唤醒等待线程是Java并发编程的核心技能,通过本文的案例,你应当掌握:

  1. synchronized + wait/notify的基础用法
  2. ReentrantLock + Condition的精准控制
  3. 使用while循环应对虚假唤醒
  4. 在生产-消费者等典型场景中避免死锁和信号丢失

最佳实践:新版代码优先使用java.util.concurrent包提供的类(如BlockingQueue),它们封装了等待/唤醒逻辑,能显著降低编码复杂度,但理解底层机制,仍是排查复杂并发问题的关键。

抱歉,评论功能暂时关闭!