# Java多线程通信wait/notify的正确姿势

1 阅读3分钟

写过多线程的都知道,线程之间的协作比想象中要难。wait和notify这对方法看起来简单,用错了能让你调一下午。

一个典型的错误示范

// 生产者-消费者,错误写法
public class WrongExample {
    private static int count = 0;
    private static final Object lock = new Object();

    public static void main(String[] args) {
        // 消费者
        new Thread(() -> {
            synchronized (lock) {
                if (count == 0) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                System.out.println("消费:" + count);
            }
        }).start();

        // 生产者
        new Thread(() -> {
            synchronized (lock) {
                count++;
                lock.notify();
            }
        }).start();
    }
}

这段代码看起来没问题,但有一个致命漏洞:如果用if判断,线程被意外唤醒时不会再检查条件。

比如系统发生了虚假唤醒(spurious wakeup),或者唤醒之后条件又不满足了,代码就会走到错误的分支。

正确写法:while循环

synchronized (lock) {
    while (count == 0) {   // 必须用while,不能用if
        lock.wait();
    }
    // 条件满足,执行逻辑
}

while的作用是:每次被唤醒后都重新检查条件,不满足就继续等。

完整的生产者消费者实现

public class BlockingQueue {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int capacity = 10;
    private final Object lock = new Object();

    public void put(int value) throws InterruptedException {
        synchronized (lock) {
            while (queue.size() == capacity) {
                lock.wait();  // 队列满了,生产者等待
            }
            queue.offer(value);
            lock.notifyAll();  // 唤醒所有等待者
        }
    }

    public int take() throws InterruptedException {
        synchronized (lock) {
            while (queue.isEmpty()) {
                lock.wait();  // 队列空了,消费者等待
            }
            int value = queue.poll();
            lock.notifyAll();  // 唤醒所有等待者
            return value;
        }
    }
}

wait和sleep的区别

对比waitsleep
所属类ObjectThread
释放锁是否
调用条件必须在synchronized块内无限制
唤醒方式notify/notifyAll或超时时间到或interrupt
用途线程间协作单纯暂停

最容易搞混的一点:wait会释放锁,sleep不会。 所以在synchronized块里sleep,其他线程根本进不来。

notify和notifyAll的区别

lock.notify();     // 随机唤醒一个等待的线程
lock.notifyAll();  // 唤醒所有等待的线程

很多人为了性能用notify,实际上大多数场景应该用notifyAll。

原因:如果等待的线程在等不同的条件,你只能唤醒一个,可能唤醒的是那些条件还不满足的线程,而真正该被唤醒的线程还在睡。

// 错误场景
synchronized (lock) {
    while (conditionA) lock.wait();  // 线程1等A
}
synchronized (lock) {
    while (conditionB) lock.wait();  // 线程2等B
}

// 如果生产者notify()随机唤醒了等A的线程,而实际上是B条件满足了
// 等B的线程永远不会被唤醒 → 死锁

用notifyAll虽然会多几次无效唤醒,但不会出错。性能损耗可以忽略。

更现代的替代方案

JDK 5之后,这些手写同步已经很少用了:

// 用BlockingQueue代替手写队列
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10);
queue.put(1);      // 满了自动阻塞
queue.take();      // 空了自动阻塞

// 用Condition实现更精细的等待
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

// 用CountDownLatch做倒计时
CountDownLatch latch = new CountDownLatch(3);

// 用CompletableFuture做异步编排
CompletableFuture.supplyAsync(() -> query())
    .thenApply(this::transform)
    .thenAccept(this::save);

除非你在维护老代码,或者面试需要,日常开发真的没必要手写wait/notify。但理解它的原理,对你理解那些并发工具类的实现非常有帮助。