本文纲要
- 生产者消费者模式概述
- 思路分析
理想情况
消费者等待
生产者等待
完整步骤 - 等待唤醒机制:
wait/notify/notifyAll - 基础代码实现(静态变量版)
- 面向对象改写(封装版)
- 阻塞队列简化实现
阻塞队列基本使用
阻塞队列取代手动等待唤醒 - 总结与编码套路
生产者消费者模式概述
生产者消费者模式是一个经典的多线程协作模式,也叫“等待唤醒机制”。
通过这个模式可以让我们更深刻地理解线程间的通信与协作。
在这个模式中有三个角色:
| 角色 | 线程 | 职责 |
|---|---|---|
| 生产者 | Cooker(厨师) | 生产数据(汉堡包) |
| 消费者 | Foodie(吃货) | 消费数据(吃汉堡包) |
| 容器 | Desk(桌子)/ 阻塞队列 | 存放数据,协调线程执行 |
核心规则:当桌上有汉堡包时,允许消费者执行(吃),不允许生产者执行;当桌上没有汉堡包时,允许生产者执行(做),不允许消费者执行。就这样,两个线程通过一个共享状态轮流执行,实现协作。
思路分析
1 ) 理想情况
一开始生产者抢到CPU,生产一个汉堡包放到桌子上;然后消费者抢到CPU,吃掉汉堡包;生产者再抢到CPU,再生产……如此交替。这是一个你一次我一次的理想执行序列。
2 ) 消费者等待(桌子为空)
如果消费者先抢到CPU,此时桌子上没有汉堡包,消费者只能等待。随后生产者抢到CPU,生产汉堡包放到桌子上,再通知(唤醒)消费者来吃。
消费者执行步骤:
- 判断桌子上是否有汉堡包
- 如果没有 → 等待
- 如果有 → 开吃,吃完后桌子状态变空,通知生产者生产,总数减一
3 ) 生产者等待(桌子已满)
如果生产者先抢到CPU,生产一个汉堡包后,又被自己再次抢到CPU(而不是消费者),此时桌上已经有汉堡包,生产者需要等待。随后消费者抢到CPU吃掉汉堡包,再通知生产者继续生产。
生产者执行步骤:
- 判断桌子上是否有汉堡包
- 如果有 → 等待
- 如果没有 → 生产汉堡包,放到桌子上,通知消费者来吃
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并传递给线程
代码改动过程
- 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 方法。
- 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();
}
}
}
}
}
}
}
- 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 对象,通过调用 desk 的 get/set 方法操作共享变量,实现了更规范的面相对象设计。
阻塞队列简化实现
1 ) 阻塞队列基本使用
ArrayBlockingQueue 是 java.util.concurrent 包下的阻塞队列实现类,底层基于数组,有界。
关键方法:
| 方法 | 说明 |
|---|---|
put(E e) | 将元素放入队列,若队列已满则阻塞等待,直到有空间 |
take() | 取出并移除队首元素,若队列为空则阻塞等待,直到有元素可取 |
继承结构:
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语句在锁的外部。 put和take方法底层已经加锁,保证了线程安全,但控制台打印的语句可能因上下文切换而连续输出多个“放”或“吃”。- 这并不影响实际生产和消费的顺序性。
总结与编码套路
通过以上三个版本的演进,可以总结出编写此类“等待唤醒”多线程协作程序的一般套路:
while(true)死循环synchronized同步锁,锁对象必须唯一- 判断共享数据是否已到结束条件(如总数已减为0),若结束则
break - 判断共享数据的状态,若满足当前线程的执行条件则执行业务,否则调用
wait()等待 - 业务执行后,修改共享状态,并调用
notifyAll()唤醒其他线程
使用 BlockingQueue 等并发工具类可以进一步简化代码,它们内部已经封装了锁和等待唤醒机制,我们只需要 put 和 take 即可。
核心思想:通过共享状态(有/无汉堡包)控制线程交替执行,实现线程间的安全通信
4950

被折叠的 条评论
为什么被折叠?



