Java基础快速入门: 生产者和消费者模式详解

本文纲要

  1. 生产者消费者模式概述
  2. 思路分析
    理想情况
    消费者等待
    生产者等待
    完整步骤
  3. 等待唤醒机制:wait/notify/notifyAll
  4. 基础代码实现(静态变量版)
  5. 面向对象改写(封装版)
  6. 阻塞队列简化实现
    阻塞队列基本使用
    阻塞队列取代手动等待唤醒
  7. 总结与编码套路

生产者消费者模式概述

生产者消费者模式是一个经典的多线程协作模式,也叫“等待唤醒机制”。
通过这个模式可以让我们更深刻地理解线程间的通信与协作。

在这个模式中有三个角色:

角色线程职责
生产者Cooker(厨师)生产数据(汉堡包)
消费者Foodie(吃货)消费数据(吃汉堡包)
容器Desk(桌子)/ 阻塞队列存放数据,协调线程执行

核心规则:当桌上有汉堡包时,允许消费者执行(吃),不允许生产者执行;当桌上没有汉堡包时,允许生产者执行(做),不允许消费者执行。就这样,两个线程通过一个共享状态轮流执行,实现协作。

思路分析

1 ) 理想情况

一开始生产者抢到CPU,生产一个汉堡包放到桌子上;然后消费者抢到CPU,吃掉汉堡包;生产者再抢到CPU,再生产……如此交替。这是一个你一次我一次的理想执行序列。

2 ) 消费者等待(桌子为空)

如果消费者先抢到CPU,此时桌子上没有汉堡包,消费者只能等待。随后生产者抢到CPU,生产汉堡包放到桌子上,再通知(唤醒)消费者来吃。

消费者执行步骤:

  1. 判断桌子上是否有汉堡包
  2. 如果没有 → 等待
  3. 如果有 → 开吃,吃完后桌子状态变空,通知生产者生产,总数减一

3 ) 生产者等待(桌子已满)

如果生产者先抢到CPU,生产一个汉堡包后,又被自己再次抢到CPU(而不是消费者),此时桌上已经有汉堡包,生产者需要等待。随后消费者抢到CPU吃掉汉堡包,再通知生产者继续生产。

生产者执行步骤:

  1. 判断桌子上是否有汉堡包
  2. 如果有 → 等待
  3. 如果没有 → 生产汉堡包,放到桌子上,通知消费者来吃

4 ) 完整步骤总结

综合两种异常情况,得到最终步骤:

消费者(吃货):

  1. 判断桌子上是否有汉堡包
  2. 如果没有 → 等待
  3. 如果有 → 开吃
  4. 吃完后,桌子状态改为没有汉堡包
  5. 唤醒等待的生产者
  6. 总数量减一

生产者(厨师):

  1. 判断桌子上是否有汉堡包
  2. 如果有 → 等待
  3. 如果没有 → 生产汉堡包,桌子状态改为有汉堡包
  4. 唤醒等待的消费者

该过程可以用以下状态图表示:

生产者生产

消费者消费

消费者等待

生产者等待

桌子空

桌子满

等待唤醒机制:wait/notify/notifyAll

Java 在 Object 类中提供了三个方法来实现线程等待与唤醒:

方法说明
wait()让当前线程释放锁并进入等待状态,直到被其他线程唤醒
notify()随机唤醒一个在此对象锁上等待的线程
notifyAll()唤醒在此对象锁上等待的所有线程

重要规则:

这三个方法必须在同步代码块(synchronized)中调用
必须使用锁对象去调用等待和唤醒方法(lock.wait()lock.notifyAll()
wait() 会释放锁,sleep() 不会释放锁

基础代码实现(静态变量版)

项目结构

src/com/wb/threaddemo014/
├── Desk.java      // 桌子类,保存共享变量 
├── Foodie.java    // 消费者线程 
├── Cooker.java    // 生产者线程 
└── Demo.java      // 测试类 

核心代码

Desk.java – 共享资源类

package com.wb.threaddemo014;
 
public class Desk {
    // 标记:true表示桌上有汉堡包, false表示桌上没有 
    public static boolean flag = false;
    // 汉堡包总数量 
    public static int count = 10;
    // 锁对象(必须唯一且不可变)
    public static final Object lock = new Object();
}

Foodie.java – 消费者线程

package com.wb.threaddemo014;
 
public class Foodie extends Thread {
    @Override 
    public void run() {
        // 步骤:
        // 1,判断桌子上是否有汉堡包。
        // 2,如果没有就等待。
        // 3,如果有就开吃 
        // 4,吃完之后,桌子上的汉堡包就没有了,叫醒等待的生产者继续生产 
        // 汉堡包的总数量减一 
 
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;          // 总数已为0,退出 
                } else {
                    if (Desk.flag) {
                        // 有汉堡包 
                        System.out.println("吃货在吃汉堡包");
                        Desk.flag = false;           // 吃完,桌子为空 
                        Desk.lock.notifyAll();       // 唤醒等待的生产者 
                        Desk.count--;                // 总数减一 
                    } else {
                        // 没有汉堡包,等待 
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

Cooker.java – 生产者线程

package com.wb.threaddemo014;
 
public class Cooker extends Thread {
    @Override 
    public void run() {
        // 生产者步骤:
        // 1,判断桌子上是否有汉堡包 
        //    如果有就等待,如果没有才生产。
        // 2,把汉堡包放在桌子上。
        // 3,叫醒等待的消费者开吃。
 
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    if (!Desk.flag) {
                        // 桌子上没有汉堡包,生产 
                        System.out.println("厨师正在生产汉堡包");
                        Desk.flag = true;            // 放上汉堡包 
                        Desk.lock.notifyAll();       // 唤醒等待的消费者 
                    } else {
                        // 桌子上有汉堡包,等待 
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

Demo.java – 启动测试

package com.wb.threaddemo014;
 
public class Demo {
    public static void main(String[] args) {
        Foodie f = new Foodie();
        Cooker c = new Cooker();
        f.start();
        c.start();
    }
}

运行结果交替打印:

厨师正在生产汉堡包
吃货在吃汉堡包
厨师正在生产汉堡包
吃货在吃汉堡包

此版本为了方便理解,使用了 static 修饰共享变量。但这种写法不够面向对象,下面进行改写。

面向对象改写(封装版)

项目结构

src/com/wb/threaddemo015/
├── Desk.java      // 重构为标准JavaBean 
├── Foodie.java    // 消费者,接收Desk对象 
├── Cooker.java    // 生产者,接收Desk对象 
└── Demo.java      // 创建Desk并传递给线程 

代码改动过程

  1. Desk.java 改为私有成员变量,提供构造方法与 get/set
package com.wb.threaddemo015;
 
public class Desk {
    private boolean flag;
    private int count;
    private final Object lock = new Object();  // 锁对象唯一且不可变 
 
    public Desk() {
        this(false, 10);  // 默认为空,总数10个 
    }
 
    public Desk(boolean flag, int count) {
        this.flag = flag;
        this.count = count;
    }
 
    public boolean isFlag() {
        return flag;
    }
 
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
 
    public int getCount() {
        return count;
    }
 
    public void setCount(int count) {
        this.count = count;
    }
 
    public Object getLock() {
        return lock;
    }
 
    @Override 
    public String toString() {
        return "Desk{" +
                "flag=" + flag +
                ", count=" + count +
                ", lock=" + lock +
                '}';
    }
}

注意:布尔类型的 getter 方法名为 isFlag,而非 getFlag;lock 由 final 修饰,没有 setter 方法。

  1. Foodie.java 和 Cooker.java 改为通过构造器接收 Desk 对象
package com.wb.threaddemo015;
 
public class Foodie extends Thread {
    private Desk desk;
 
    public Foodie(Desk desk) {
        this.desk = desk;
    }
 
    @Override 
    public void run() {
        while (true) {
            synchronized (desk.getLock()) {
                if (desk.getCount() == 0) {
                    break;
                } else {
                    if (desk.isFlag()) {
                        System.out.println("吃货在吃汉堡包");
                        desk.setFlag(false);
                        desk.getLock().notifyAll();
                        // count减一:先获取当前值,减一后再设置 
                        desk.setCount(desk.getCount() - 1);
                    } else {
                        try {
                            desk.getLock().wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}
package com.wb.threaddemo015;
 
public class Cooker extends Thread {
 
    private Desk desk;
 
    public Cooker(Desk desk) {
        this.desk = desk;
    }
 
    @Override 
    public void run() {
        while (true) {
            synchronized (desk.getLock()) {
                if (desk.getCount() == 0) {
                    break;
                } else {
                    if (!desk.isFlag()) {
                        System.out.println("厨师正在生产汉堡包");
                        desk.setFlag(true);
                        desk.getLock().notifyAll();
                    } else {
                        try {
                            desk.getLock().wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}
  1. Demo.java 创建 Desk 对象并共享给两个线程
package com.wb.threaddemo015;
 
public class Demo {
    public static void main(String[] args) {
        Desk desk = new Desk();  // 空参构造,默认flag=false, count=10 
 
        Foodie f = new Foodie(desk);
        Cooker c = new Cooker(desk);
 
        f.start();
        c.start();
    }
}

要点:两个线程共享同一个 Desk 对象,通过调用 deskget/set 方法操作共享变量,实现了更规范的面相对象设计。

阻塞队列简化实现

1 ) 阻塞队列基本使用

ArrayBlockingQueuejava.util.concurrent 包下的阻塞队列实现类,底层基于数组,有界。
关键方法:

方法说明
put(E e)将元素放入队列,若队列已满则阻塞等待,直到有空间
take()取出并移除队首元素,若队列为空则阻塞等待,直到有元素可取

继承结构:

«interface»

Collection

«interface»

Iterable

«interface»

Queue

«interface»

BlockingQueue

ArrayBlockingQueue

LinkedBlockingQueue

Demo 示例:

package com.wb.threaddemo016;
 
import java.util.concurrent.ArrayBlockingQueue;
 
public class Demo02 {
    public static void main(String[] args) throws Exception {
        // 创建阻塞队列,容量为1(只能放一个汉堡包)
        ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);
 
        // 存储元素 
        arrayBlockingQueue.put("汉堡包");
 
        // 取元素 
        System.out.println(arrayBlockingQueue.take());  // 输出:汉堡包 
        System.out.println(arrayBlockingQueue.take());  // 此时队列为空,会阻塞等待 
 
        System.out.println("程序结束了");  // 这行不会执行 
    }
}

2 ) 阻塞队列实现等待唤醒

用阻塞队列替代手动 wait/notify,代码会非常简洁。

项目结构

src/com/wb/threaddemo017/
├── Cooker.java    // 生产者线程,直接调用bd.put()
├── Foodie.java    // 消费者线程,直接调用bd.take()
└── Demo.java      // 创建队列并传递给线程 

核心代码

Cooker.java

package com.wb.threaddemo017;
 
import java.util.concurrent.ArrayBlockingQueue;
 
public class Cooker extends Thread {
    private ArrayBlockingQueue<String> bd;
 
    public Cooker(ArrayBlockingQueue<String> bd) {
        this.bd = bd;
    }
 
    @Override 
    public void run() {
        while (true) {
            try {
                bd.put("汉堡包");
                System.out.println("厨师放入一个汉堡包");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Foodie.java

package com.wb.threaddemo017;
 
import java.util.concurrent.ArrayBlockingQueue;
 
public class Foodie extends Thread {
    private ArrayBlockingQueue<String> bd;
 
    public Foodie(ArrayBlockingQueue<String> bd) {
        this.bd = bd;
    }
 
    @Override 
    public void run() {
        while (true) {
            try {
                String take = bd.take();
                System.out.println("吃货将" + take + "拿出来吃了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Demo.java

package com.wb.threaddemo017;
 
import java.util.concurrent.ArrayBlockingQueue;
 
public class Demo {
    public static void main(String[] args) {
        // 容量为1,实现放一个拿一个 
        ArrayBlockingQueue<String> bd = new ArrayBlockingQueue<>(1);
 
        Foodie f = new Foodie(bd);
        Cooker c = new Cooker(bd);
 
        f.start();
        c.start();
    }
}

运行结果说明:

  • 输出可能不是严格的“放-吃-放-吃”交替,因为 println 语句在锁的外部。
  • puttake 方法底层已经加锁,保证了线程安全,但控制台打印的语句可能因上下文切换而连续输出多个“放”或“吃”。
  • 这并不影响实际生产和消费的顺序性。

总结与编码套路

通过以上三个版本的演进,可以总结出编写此类“等待唤醒”多线程协作程序的一般套路:

  1. while(true) 死循环
  2. synchronized 同步锁,锁对象必须唯一
  3. 判断共享数据是否已到结束条件(如总数已减为0),若结束则 break
  4. 判断共享数据的状态,若满足当前线程的执行条件则执行业务,否则调用 wait() 等待
  5. 业务执行后,修改共享状态,并调用 notifyAll() 唤醒其他线程

使用 BlockingQueue 等并发工具类可以进一步简化代码,它们内部已经封装了锁和等待唤醒机制,我们只需要 puttake 即可。

核心思想:通过共享状态(有/无汉堡包)控制线程交替执行,实现线程间的安全通信

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值