20.0、Java多线程——线程协作-->生产者/消费者模式
线程通信:
应用场景:生产者和消费者问题
假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费
如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,知道仓库中的产品被消费者取走为止
如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止
线程通信-分析
这是一个线程同步问题,生产者和消费者共享一个资源,并且生产者和消费者相互依赖,互为条件
对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费
对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
在生产者消费者问题中,仅有 synchronized 是不够的,
因为 synchronized 可阻止并发更新同一个共享资源,实现同步
但是 synchronized 不能用来实现不同线程之间的消息传递(通信)
那应该如何解决线程通信呢?
Java提供了几个方法来解决线程之间的通信问题,如下所示:
方法名 | 作用 |
wait() | 表示线程一直等待,直到其他线程通知,与sleep不同的是 他会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度 |
注意:均是 Object类 的方法,都只能在 同步方法 或者 同步代码块 中使用,否则会抛出异常 IllegalMonitorStateException
解决方式1:
并发协作模型 “生产者 / 消费者模式” ---> 管程法
生产者:负责生产数据的模块(可能是 方法、对象、线程、进程);
消费者:负责处理数据的模块(可能是 方法、对象、线程、进程);
缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”,生产者将生产好的数据放入缓存区,消费者从缓冲区拿出数据
我们用代码来实现,看看如何测试,代码如下所示:
TestPC.java文件:
package com.hkl.threadCall;
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Producer(container).start();
new Consumer(container).start();
}
}
//生产者
class Producer extends Thread {
SynContainer container;
public Producer(SynContainer container) {
this.container = container;
}
// 生产
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("生产了"+i+"只鸡");
try {
container.push(new Chicken(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者
class Consumer extends Thread {
SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
// 消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
System.out.println("消费了-->"+container.pop().id+"只鸡");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//产品
class Chicken {
int id;
public Chicken(int id) {
this.id = id;
}
}
//数据缓冲区
class SynContainer {
// 需要一个规定好大小的容器
Chicken chickens[] = new Chicken[10];
// 容器计数器
private int count = 0;
// 生产者放入产品
public synchronized void push(Chicken chicken) throws InterruptedException {
//如果容器满了,就需要消费者去消费产品
if(count == chickens.length) {
// 通知消费者消费,生产等待
this.wait();
}
//如果没有满,我们就要丢入产品
chickens[count] = chicken;
count++;
//可以通知消费者消费了
this.notifyAll();
}
// 消费者拿走产品
public synchronized Chicken pop() throws InterruptedException {
// 判断能否消费
if(count == 0) {
// 等待生产者生产,消费者等待
this.wait();
}
// 如果可以消费
count--;
Chicken chicken = chickens[count];
// 吃完了,通知生产者生产
this.notifyAll();
return chicken;
}
}
生产者 生产100只鸡,消费者 吃掉100只鸡,缓冲池容量 10
两条线程同时开始运行,
消费者发现缓冲池中没有鸡所以停下来 wait() 等待并告诉生产者生产,
生产者发现缓冲池中鸡的数量小于容量开始生产,
生产10只鸡后,生产者发现缓冲池满了停下来 wait() 等待,并告诉消费者吃鸡
然后消费者被唤醒发现缓冲池中有鸡开始吃鸡,吃完了 wait() 等待并告诉生产者吃完了开始生产
最后一致循环往复直到100只鸡生产完、100只鸡被消费者吃完
解决方式2:
并发协作模型“生产者 / 消费者模式” ---> 信号灯法
比如说我们可以设置一个 标志位flag,当 flag = true 的时候让该线程去 wait()等待,当 flag = false 的时候让该线程去通知唤醒另外一条线程
接下来我们用代码来测试一下,如下所示:
TestPC2.java文件:
package com.hkl.threadCall;
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
//生产者->演员
class Player extends Thread {
private TV tv;
public Player(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
tv.play("警察故事~");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者-->观众
class Watcher extends Thread {
private TV tv;
public Watcher(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
tv.watch();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//产品-->节目
class TV {
// 演员表演,观众等待 T
// 观众观看,演员等待 F
String voice; //表演的节目
boolean flag = true;
//表演
public synchronized void play(String voice) throws InterruptedException {
if(!flag) {
this.wait();
}
System.out.println("演员表演了:"+voice);
// 通知观众观看
this.notifyAll();//通知唤醒
this.voice = voice;
this.flag = !this.flag;
}
// 观看
public synchronized void watch() throws InterruptedException {
if(flag) {
this.wait();
}
System.out.println("观看了:"+voice);
// 通知演员表演
this.notifyAll();
this.flag = !this.flag;
}
}
两条线程同时开运行:
设置一个标志位 flag ,当 flag = true 的时候->演员演、观众等 ,flag = false 的时候演员等,观众看
开始 flag = true 演员线程开始表演,观众因 flag = false 在 wait() 等待
演员拍摄结束 让 flag = false 并通知唤醒 观众 开始看
观众 被唤醒发现 flag = false 开始看表演,而 演员 因为 flag = false 所以进入 wait() 等待
观众 看完表演之后将 flag = true 并通知唤醒 演员 -->表演看完了可以开始拍摄了
然后 演员 被通知唤醒 发现 flag = true 开始拍摄表演,观众 因flag = true 而进入wait() 等待
最终如此循环往复 直到 演员 表演完 10 次, 观众 看完 10次 程序结束
可以看到程序的运行截图如下所示:
当然解决的方式还有很多远远不止这些......