Java生产者与消费者:解决多线程通信混乱

Chloe
Chloe
发布时间:2026-10-16| 2人浏览过
原创

在多线程环境中,生产者和消费者共享同一数据缓冲区时,极易因调度无序导致数据错乱。本文通过经典的“厨师做菜、服务员上菜”案例,演示如何从初始的线程竞争状态,逐步利用 synchronized 保证数据一致性,再借助 wait() 和 notify() 实现线程的精确交替唤醒,彻底解决生产消费不同步的问题。

概述与问题场景

生产者与消费者问题(Producer-consumer problem)是多线程同步的经典案例,通常也被称为有限缓冲问题。其核心在于两个线程——生产者和消费者——共享固定大小的缓冲区。若缺乏有效的协调机制,会导致以下问题:

  1. 数据覆盖或错乱: 生产者写入数据未完整时,消费者读取了不完整的数据;或者生产者连续写入覆盖旧数据,而消费者未及时取走。
  2. 资源浪费: 缓冲区空时消费者继续等待,缓冲区满时生产者继续尝试存入。

该过程可以形象地类比为一个餐厅场景:厨师(生产者)负责做菜,服务员(消费者)负责端菜。假设厨房只有一个盘子用于暂存菜品。正确的协作流程应当是:厨师做完一道菜后通知服务员,服务员端走这道菜并通知厨师开始做下一道。在此过程中,双方必须交替工作,确保任何时候要么只有厨师在做饭,要么只有服务员在端盘子,从而保障数据的绝对安全。

第一阶段:重现线程不安全

为了直观展示多线程通信中存在的问题,我们首先实现一个没有进行任何同步控制的版本。在这个阶段,我们将看到即使有简单的模拟耗时操作,也会导致严重的“串味”现象。

第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多线程通信技巧的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。