Java生产者与消费者:解决多线程通信混乱
在多线程环境中,生产者和消费者共享同一数据缓冲区时,极易因调度无序导致数据错乱。本文通过经典的“厨师做菜、服务员上菜”案例,演示如何从初始的线程竞争状态,逐步利用 synchronized 保证数据一致性,再借助 wait() 和 notify() 实现线程的精确交替唤醒,彻底解决生产消费不同步的问题。
概述与问题场景
生产者与消费者问题(Producer-consumer problem)是多线程同步的经典案例,通常也被称为有限缓冲问题。其核心在于两个线程——生产者和消费者——共享固定大小的缓冲区。若缺乏有效的协调机制,会导致以下问题:
- 数据覆盖或错乱: 生产者写入数据未完整时,消费者读取了不完整的数据;或者生产者连续写入覆盖旧数据,而消费者未及时取走。
- 资源浪费: 缓冲区空时消费者继续等待,缓冲区满时生产者继续尝试存入。
该过程可以形象地类比为一个餐厅场景:厨师(生产者)负责做菜,服务员(消费者)负责端菜。假设厨房只有一个盘子用于暂存菜品。正确的协作流程应当是:厨师做完一道菜后通知服务员,服务员端走这道菜并通知厨师开始做下一道。在此过程中,双方必须交替工作,确保任何时候要么只有厨师在做饭,要么只有服务员在端盘子,从而保障数据的绝对安全。
第一阶段:重现线程不安全
为了直观展示多线程通信中存在的问题,我们首先实现一个没有进行任何同步控制的版本。在这个阶段,我们将看到即使有简单的模拟耗时操作,也会导致严重的“串味”现象。
第1步:定义共享资源类 Food
Food 类作为缓冲区,包含菜名 name 和口味 taste。为了模拟实际生产过程中的延迟,我们在设置口味时人为增加休眠时间,这加剧了线程切换时的可见性问题。
static class Food {
private String name;
private String taste;
public void setNameAneTaste(String name, String taste) {
this.name = name;
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
this.taste = taste;
}
public void get() {
System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
}
}
第2步:创建生产者线程 Cook
厨师线程循环生成100道菜,每隔一次交替制作“小米粥”和“老北京鸡肉卷”。
static class Cook extends Thread {
private Food f;
public Cook(Food f) { this.f = f; }
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
f.setNameAneTaste("小米粥", "没味道,不好吃");
} else {
f.setNameAneTaste("老北京鸡肉卷", "甜辣味");
}
}
}
}
第3步:创建消费者线程 Waiter
服务员线程同样循环100次,每次先短暂休眠,然后调用 get() 端走菜品。
static class Waiter extends Thread {
private Food f;
public Waiter(Food f) { this.f = f; }
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
f.get();
}
}
}
第4步:启动测试
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
运行上述代码,结果将呈现明显的数据错乱。例如,你可能会看到输出:
“服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃”。这是因为在厨师设置了 name 并进入 sleep 期间,CPU 时间片被切换到服务员线程,服务员读取到了尚未更新的 taste 字段。
第二阶段:加入线程安全 synchronized
为了解决数据读写不同步的问题,我们对 Food 类中的 setNameAneTaste 和 get 方法添加 synchronized 关键字。这将把这两个方法的执行变成互斥操作,确保在同一时刻只能有一个线程进入这些方法。
public synchronized void setNameAneTaste(String name, String taste) { ... }
public synchronized void get() { ... }
再次运行程序,你会发现数据错乱的现象消失了,菜名和口味能够一一对应。然而,新的问题出现了:生产与消费不再交替进行。由于 synchronized 只保证了互斥,并没有控制线程的唤醒顺序,厨师可能会在短时间内连续生产多道菜(因为锁释放后它依然可能立即重新获得锁),服务员也可能会连续多次读取同一个数据(如果缓冲区被视为无限大)。这不符合生产者-消费者模型中严格的“生产一个、消费一个”的交替要求。
第三阶段:实现精准的线程通信
要实现严格的生产者-消费者逻辑,必须在保持线程安全的基础上,引入线程阻塞和唤醒机制。Java 中通过 java.lang.Object 提供的 wait() 和 notifyAll() 方法来实现这一点。
wait():使当前线程进入等待状态,并释放对象锁,允许其他线程获取锁。notifyAll():唤醒在该对象监视器上等待的所有线程。
第1步:引入状态标记 flag
在 Food 类中添加布尔变量 flag 来控制流转:
true:表示厨师可以生产,此时服务员应等待。false:表示厨师已生产完毕,此时服务员可以消费,厨师应等待。
private boolean flag = true;
第2步:修改生产者线程逻辑
在 setNameAneTaste 中判断 flag。如果 flag 为 false(说明还没被消费过),则调用 wait() 让厨师休眠并释放锁。生产完成后,将 flag 设为 false,调用 notifyAll() 唤醒服务员,然后自己也进入 wait() 状态。
public synchronized void setNameAneTaste(String name, String taste) {
if (!flag) { // 如果服务员还没端走,厨师等待
try { wait(); } catch (InterruptedException e) { e.printStackTrace(); }
}
this.name = name;
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
this.taste = taste;
flag = false; // 标记为已生产,服务员可消费
notifyAll(); // 唤醒正在等待的服务员
try { wait(); } catch (InterruptedException e) { e.printStackTrace(); }
}
第3步:修改消费者线程逻辑
在 get() 中判断 flag。如果 flag 为 true(说明还没开始生产),则调用 wait() 让服务员休眠。消费完成后,将 flag 设为 true,调用 notifyAll() 唤醒厨师,然后自己也进入 wait() 状态。
public synchronized void get() {
if (flag) { // 如果厨师还没做,服务员等待
try { wait(); } catch (InterruptedException e) { e.printStackTrace(); }
}
System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
flag = true; // 标记为已消费,厨师可生产
notifyAll(); // 唤醒正在等待的厨师
try { wait(); } catch (InterruptedException e) { e.printStackTrace(); }
}
完成判断与总结
经过上述改造后,程序的输出将呈现出完美的交替状态:“小米粥”总是对应“没味道”,“老北京鸡肉卷”总是对应“甜辣味”,且每一步生产和消费都紧密衔接。这就是通过 synchronized、wait() 和 notifyAll() 三者配合,成功解决 Java 多线程通信中生产者与消费者问题的标准范式。
以上就是Java中使用synchronized和wait/notify解决生产者消费者问题的详细内容,更多关于Java多线程通信技巧的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













