写过多线程的都知道,线程之间的协作比想象中要难。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的区别
| 对比 | wait | sleep |
|---|---|---|
| 所属类 | Object | Thread |
| 释放锁 | 是 | 否 |
| 调用条件 | 必须在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。但理解它的原理,对你理解那些并发工具类的实现非常有帮助。