汉堡店里的线程通信:wait/notify机制详解

54 阅读5分钟

将通过一个"汉堡店厨房"的故事,生动解释如何使用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() 操作步骤

  1. ​释放锁​​:将当前线程从Owner移出
  2. ​加入等待集​​:将线程加入_WaitSet队列
  3. ​线程挂起​​:调用park()挂起线程
  4. ​等待通知​​:等待其他线程调用notify()

notify() 操作步骤

  1. ​从等待集移出​​:从_WaitSet中取出一个线程
  2. ​加入入口队列​​:将该线程加入_EntryList
  3. ​唤醒线程​​:调用unpark()唤醒线程
  4. ​重新竞争锁​​:被唤醒线程需要重新竞争锁

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()超时结束
使用位置同步块内任意位置
所属类ObjectThread

Q: 为什么notify()可能不唤醒目标线程?

因为notify()随机唤醒一个线程,可能唤醒的是:

  1. 同类型的线程(如唤醒厨师而不是服务员)
  2. 被虚假唤醒的线程(即使没有通知也可能唤醒)

Q: 如何避免"丢失通知"问题?

java
Copy
// 正确顺序:
synchronized(lock) {
    // 1. 修改条件
    condition = true;
    
    // 2. 再发送通知
    lock.notifyAll();
}

// 错误顺序(通知可能丢失):
synchronized(lock) {
    // 1. 先发送通知
    lock.notifyAll();
    
    // 2. 再修改条件
    condition = true; // 此时等待线程可能错过通知
}

总结:wait/notify最佳实践

  1. ​同步保护​​:始终在synchronized块内使用
  2. ​循环检查​​:用while代替if检查等待条件
  3. ​通知所有​​:优先使用notifyAll()
  4. ​资源释放​​:wait()会自动释放锁
  5. ​中断处理​​:正确处理InterruptedException
  6. ​超时机制​​:使用wait(long)防止永久等待

在汉堡店的例子中:

  • 👨‍🍳 厨师制作汉堡时,如果柜台满了就wait()
  • 🔔 制作完成时notifyAll()通知服务员
  • 👩‍🍳 服务员取汉堡时,如果柜台空了就wait()
  • 🔔 取走汉堡时notifyAll()通知厨师

这个机制确保了汉堡店高效运转,厨师和服务员完美配合,避免了柜台溢出或空置的情况。这正是wait/notify在Java线程通信中的价值体现!