将通过一个"汉堡店厨房"的故事,生动解释如何使用wait/notify实现线程间通信。想象一个繁忙的汉堡店,厨师(生产者线程)在制作汉堡,服务员(消费者线程)在取汉堡,而柜台就是他们通信的共享区。
故事设定:汉堡店厨房
- 🍔 汉堡柜台:共享资源(对象锁)
- 👨🍳 厨师线程:生产者(调用wait/notify)
- 👩🍳 服务员线程:消费者(调用wait/notify)
- 🔔 通知铃:notify/notifyAll机制
- 📝 订单系统:等待队列
基础原理:等待通知机制
核心方法签名
java
Copy
public final void wait() throws InterruptedException;
public final void wait(long timeout) throws InterruptedException;
public final void notify();
public final void notifyAll();
代码实现:汉堡店运营系统
共享资源:汉堡柜台
java
Copy
public class BurgerCounter {
private int burgerCount = 0; // 汉堡数量
private final int MAX_BURGERS = 5; // 柜台容量
// 厨师放汉堡
public synchronized void putBurger() throws InterruptedException {
while (burgerCount >= MAX_BURGERS) {
System.out.println("柜台满了,厨师等待中...");
wait(); // 柜台满,厨师等待
}
burgerCount++;
System.out.println("厨师制作汉堡,柜台汉堡数: " + burgerCount);
notifyAll(); // 通知所有服务员
}
// 服务员取汉堡
public synchronized void takeBurger() throws InterruptedException {
while (burgerCount == 0) {
System.out.println("柜台空了,服务员等待中...");
wait(); // 柜台空,服务员等待
}
burgerCount--;
System.out.println("服务员取走汉堡,柜台汉堡数: " + burgerCount);
notifyAll(); // 通知所有厨师
}
}
厨师线程(生产者)
java
Copy
public class Chef implements Runnable {
private BurgerCounter counter;
public Chef(BurgerCounter counter) {
this.counter = counter;
}
@Override
public void run() {
try {
while (true) {
counter.putBurger();
Thread.sleep(1000); // 制作汉堡需要时间
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
服务员线程(消费者)
java
Copy
public class Waiter implements Runnable {
private BurgerCounter counter;
public Waiter(BurgerCounter counter) {
this.counter = counter;
}
@Override
public void run() {
try {
while (true) {
counter.takeBurger();
Thread.sleep(2000); // 服务顾客需要时间
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
启动汉堡店
java
Copy
public class BurgerShop {
public static void main(String[] args) {
BurgerCounter counter = new BurgerCounter();
// 创建2名厨师
for (int i = 1; i <= 2; i++) {
new Thread(new Chef(counter), "厨师" + i).start();
}
// 创建3名服务员
for (int i = 1; i <= 3; i++) {
new Thread(new Waiter(counter), "服务员" + i).start();
}
}
}
运行结果示例
Copy
厨师1制作汉堡,柜台汉堡数: 1
厨师2制作汉堡,柜台汉堡数: 2
服务员1取走汉堡,柜台汉堡数: 1
服务员2取走汉堡,柜台汉堡数: 0
柜台空了,服务员3等待中...
厨师1制作汉堡,柜台汉堡数: 1
服务员3取走汉堡,柜台汉堡数: 0
厨师2制作汉堡,柜台汉堡数: 1
...
底层原理:JVM的等待队列机制
JVM对象监视器结构
c
Copy
class ObjectMonitor {
Thread* _owner; // 当前持有锁的线程
ObjectWaiter* _EntryList; // 等待获取锁的线程队列
ObjectWaiter* _WaitSet; // 调用了wait()的线程队列
volatile int _count; // 重入次数
};
wait() 操作步骤
- 释放锁:将当前线程从Owner移出
- 加入等待集:将线程加入_WaitSet队列
- 线程挂起:调用park()挂起线程
- 等待通知:等待其他线程调用notify()
notify() 操作步骤
- 从等待集移出:从_WaitSet中取出一个线程
- 加入入口队列:将该线程加入_EntryList
- 唤醒线程:调用unpark()唤醒线程
- 重新竞争锁:被唤醒线程需要重新竞争锁
notifyAll() 操作步骤
java
Copy
public final void notifyAll() {
// 遍历整个等待集
for (ObjectWaiter waiter = _WaitSet; waiter != null; waiter = waiter._next) {
// 将每个等待线程移出等待集
removeFromWaitSet(waiter);
// 加入入口队列
addToEntryList(waiter);
// 唤醒线程
unpark(waiter._thread);
}
}
关键注意事项
1. 必须在同步块内使用
java
Copy
// 错误示例!
public void takeBurger() {
// 没有同步块
wait(); // 抛出IllegalMonitorStateException
}
2. 使用while循环检查条件
java
Copy
// 正确做法
while (burgerCount == 0) {
wait();
}
// 危险做法(虚假唤醒问题)
if (burgerCount == 0) {
wait();
}
3. 使用notifyAll()更安全
java
Copy
// 使用notify()可能的问题
notify(); // 只唤醒一个线程,可能唤醒的是同一类型的线程
// 更推荐的做法
notifyAll(); // 唤醒所有等待线程
4. 处理中断异常
java
Copy
try {
wait();
} catch (InterruptedException e) {
// 恢复中断状态
Thread.currentThread().interrupt();
// 处理中断逻辑
}
高级应用:多条件等待
当有多个等待条件时,可以使用多个Condition对象:
java
Copy
public class AdvancedBurgerCounter {
private final Lock lock = new ReentrantLock();
private final Condition chefCondition = lock.newCondition();
private final Condition waiterCondition = lock.newCondition();
public void putBurger() {
lock.lock();
try {
while (burgerCount >= MAX_BURGERS) {
chefCondition.await(); // 厨师专用等待
}
burgerCount++;
waiterCondition.signalAll(); // 只唤醒服务员
} finally {
lock.unlock();
}
}
public void takeBurger() {
lock.lock();
try {
while (burgerCount == 0) {
waiterCondition.await(); // 服务员专用等待
}
burgerCount--;
chefCondition.signalAll(); // 只唤醒厨师
} finally {
lock.unlock();
}
}
}
性能优化技巧
1. 减少通知次数
java
Copy
// 只在状态变化时通知
if (burgerCount == 0) {
notifyAll(); // 从空变为有汉堡时才通知
}
if (burgerCount == MAX_BURGERS) {
notifyAll(); // 从满变为不满时通知
}
2. 使用超时等待
java
Copy
public void putBurgerWithTimeout() throws InterruptedException {
synchronized(this) {
long start = System.currentTimeMillis();
long waitTime = 5000; // 5秒超时
while (burgerCount >= MAX_BURGERS) {
long remaining = waitTime - (System.currentTimeMillis() - start);
if (remaining <= 0) {
throw new TimeoutException("厨师等待超时");
}
wait(remaining); // 带超时的等待
}
// ... 制作汉堡
}
}
3. 避免嵌套通知
java
Copy
// 危险:可能导致死锁
synchronized(lockA) {
synchronized(lockB) {
lockB.wait();
lockA.notify();
}
}
// 安全做法:按固定顺序获取锁
常见问题解答
Q: wait()和sleep()有什么区别?
| 特性 | wait() | sleep() |
|---|---|---|
| 锁释放 | ✅ 释放锁 | ❌ 不释放锁 |
| 唤醒方式 | notify()/notifyAll() | 超时结束 |
| 使用位置 | 同步块内 | 任意位置 |
| 所属类 | Object | Thread |
Q: 为什么notify()可能不唤醒目标线程?
因为notify()随机唤醒一个线程,可能唤醒的是:
- 同类型的线程(如唤醒厨师而不是服务员)
- 被虚假唤醒的线程(即使没有通知也可能唤醒)
Q: 如何避免"丢失通知"问题?
java
Copy
// 正确顺序:
synchronized(lock) {
// 1. 修改条件
condition = true;
// 2. 再发送通知
lock.notifyAll();
}
// 错误顺序(通知可能丢失):
synchronized(lock) {
// 1. 先发送通知
lock.notifyAll();
// 2. 再修改条件
condition = true; // 此时等待线程可能错过通知
}
总结:wait/notify最佳实践
- 同步保护:始终在
synchronized块内使用 - 循环检查:用while代替if检查等待条件
- 通知所有:优先使用notifyAll()
- 资源释放:wait()会自动释放锁
- 中断处理:正确处理InterruptedException
- 超时机制:使用wait(long)防止永久等待
在汉堡店的例子中:
- 👨🍳 厨师制作汉堡时,如果柜台满了就
wait() - 🔔 制作完成时
notifyAll()通知服务员 - 👩🍳 服务员取汉堡时,如果柜台空了就
wait() - 🔔 取走汉堡时
notifyAll()通知厨师
这个机制确保了汉堡店高效运转,厨师和服务员完美配合,避免了柜台溢出或空置的情况。这正是wait/notify在Java线程通信中的价值体现!