线程之间有哪些通信方式?wait/notify 为什么要配合锁?

#Java#面试八股

线程之间有哪些通信方式?wait/notify 为什么要配合锁?

结论先行:Java 线程通信只有两大类——共享内存 + 同步(wait/notify、Condition、volatile 标志位)和消息传递(BlockingQueue、管道流)。wait/notify 必须配合锁,是因为"检查条件"和"进入等待"这两步必须原子,否则会丢唤醒。

1. 四种通信方式

方式实现适用注意
wait/notifyObject 的监视器经典生产者-消费者必须在 synchronized 内,用 while 判条件
ConditionReentrantLock.newCondition()需要多组等待队列await/signal 替代 wait/notify
阻塞队列BlockingQueue生产消费解耦生产环境首选,天然线程安全
管道 / 协调工具PipedInputStream、CountDownLatch、CompletableFuture流式传输、任务编排各有适用场景,别硬套

2. 为什么 wait 必须持有锁

假设允许"不持锁就 wait",会出现这个漏洞:

时刻 t1  消费者检查 buffer 为空 → 准备 wait
时刻 t2  生产者放入元素 → notify()(此时没人等待,信号丢失)
时刻 t3  消费者执行 wait() → 永远等下去

"检查条件"与"进入等待"之间存在窗口。把这两步放进 synchronized 后,生产者要 notify 也必须先拿到同一把锁,于是不可能在窗口期插入——这就是 wait/notify 强制要求持有 monitor 的原因(不持有会抛 IllegalMonitorStateException)。

3. 标准模板:必须用 while 判断

public class BoundedBuffer<T> {
    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;

    public BoundedBuffer(int capacity) { this.capacity = capacity; }

    public synchronized void put(T item) throws InterruptedException {
        while (queue.size() == capacity) {   // 用 while,不是 if
            wait();                          // 释放锁并挂起
        }
        queue.add(item);
        notifyAll();                         // 唤醒所有等待者(含消费者)
    }

    public synchronized T take() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();
        }
        T item = queue.poll();
        notifyAll();
        return item;
    }
}

为什么是 while 而不是 if,有两个硬理由:

  1. 虚假唤醒(spurious wakeup):JVM 允许 wait() 在没有 notify 的情况下返回,醒来后条件可能仍不满足;
  2. 被唤醒后要重新竞争锁:在等待队列里醒来的线程不保证马上拿到锁,等它拿到时条件可能又变了。

4. 生产环境更推荐 BlockingQueue

BlockingQueue<Order> queue = new LinkedBlockingQueue<>(1000);

// 生产者
queue.put(order);          // 队列满则阻塞,天然背压
// 消费者
Order o = queue.take();    // 队列空则阻塞

// 或者用线程池直接消费
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        Order o = queue.take();
        handle(o);
    }
});

BlockingQueue 内部已经把 ReentrantLock + 两个 Condition(notEmpty、notFull)封装好了,比自己写 wait/notify 少踩一堆坑。

5. 常见追问

追问一:notify 和 notifyAll 怎么选? 优先 notifyAll。notify 只唤醒一个线程,如果唤醒的是"条件不满足"的那类线程(比如生产者唤醒了另一个生产者),真正的消费者还在睡,就会假死。notifyAll 多一次无效唤醒,但安全。

追问二:wait 和 sleep 有什么区别? wait 是 Object 方法、释放锁、需要 notify 唤醒、必须在同步块内;sleep 是 Thread 静态方法、不释放锁、只等超时。一句话:wait 让出锁等人叫,sleep 抱着锁睡到点。

追问三:Condition 比 wait/notify 好在哪? 一个 Lock 可以创建多个 Condition,实现精确唤醒。比如有界队列里 notEmpty 只唤醒消费者、notFull 只唤醒生产者,避免了 notifyAll 的惊群。

追问四:volatile 能做线程通信吗? 能做"标志位"式的单向通知(如 volatile boolean stop),但无法表达"等待某个条件成立"。需要挂起/唤醒时必须用 wait/notify、Condition 或 LockSupport.park/unpark。

追问五:LockSupport 和 wait/notify 的区别? LockSupport.park/unpark 以线程为对象、不需要持有锁、unpark 可以先于 park 调用(有许可计数),不会丢信号;wait/notify 以对象 monitor 为对象、必须先持锁、信号可能丢失。

小结

线程通信的本质是"等一个条件成立"。wait/notify 把"判断条件 + 挂起"放进锁里保证原子,while 循环负责醒来后复查,BlockingQueue 则把这套逻辑封装成了开箱即用的组件。

笔记加载中…