生产者消费者模式
一、介绍
生产者消费者模式是一种设计模式,开发中我们需要一种这样的场景:
- 一个模块用于产生数据,称为生产模块
- 一个模块用于消费生产模块生成的数据,称为消费模块
处理这些模块,可以是函数,可以是线程或者进程,我们把处理生产模块的角色称为生产者,处理消费模块的角色称为消费者
但是在这之中,最重要的不是生产者也不是消费者,而是数据。这个数据到底如何定义:
- 必须是共享的数据
- 生产、消费模块对其进行处理时,应该要达到“相反”功能,也就是对数据处理时需要变化
则数据是对:消费者、生产者 ,两者进行解耦。两方只关心数据,而不与对方产生直接依赖,降低耦合性
1.1、使用传统wait、notify实现生产者消费者模式
下面案例实现一个生产者消费者模式,通过操作指定对象的成员数据(对象内唯一 / 共享)
通过wait
、notifyAll
进行通知生产消费,对其操作成员数据
新学到的玩意:
- wait:使当前线程释放锁并进入等待状态,直到唤醒再继续执行
- notifyAll:唤醒所有等待的线程,然后当前线程继续执行,不释放锁,除非遇到
wait
,或者代码块Over- notify:只唤醒一个等待的线程
package com.migu;
public class ProducerAndConsumer {
public static void main(String[] args) {
Data data = new Data(); // 共享数据
// 生产者线程
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 消费者线程
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
// 数据资源
class Data {
private int num;
public synchronized void increment () throws InterruptedException {
// 资源“过多”就暂停生产数据, 通知 其他线程消费数据
while (num != 0) {
this.wait(); // 释放锁,进入等待队列
}
num++;
System.out.println(Thread.currentThread().getName() + "生产之后: " + num);
this.notifyAll(); // 通知/唤醒 除当前线程的其他所有线程
}
public synchronized void decrement () throws InterruptedException {
// 没资源了就暂停消费数据 通知 其他线程去产生数据
while (num == 0) {
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "消费之后: " + num);
this.notifyAll(); // 通知/唤醒 除当前线程的其他所有线程
}
}
以上案例注意点:
- 要注意的是:锁的是什么
- 使用
wait
和notify
时,当前线程必须持有对应的对象锁,此时对象才可以调用这些方法 - 否则会抛出
java.lang.IllegalMonitorStateException
异常:非法监视器状态异常 - 以下案例中: 由于
Synchronized
关键字应用于成员方法,所以锁的是Data
对象,那么在当前的同步代码中,只有Data
对象才可以调用wait
andnotifyAll
。
- 使用
- 还有一个重要的问题:虚假唤醒
- 虚假唤醒指的是:当存在多个线程时,生产者线程只产生了一份内容时,却导致了大量的消费者线程唤醒,并执行消费。
- 所以在如上案例中,判断
num
的状态,我并没有使用if
语句,而是采用了while
。否则num
状态就可能为负数 - 原因是:
- 因为
if
只会执行一次,执行完会接着向下执行if
语句外边的代码 - 而
while
不会,直到条件满足才会结束判断的
- 因为
- 所以:等待应该总是出现在循环中
但是以上案例有个缺点,就是在多个线程操作时的环境下,想要实现顺序执行时无法实现的
1.2、Lock版实现生产者消费者问题
一、Interface Condition介绍
Condition
接口也位于java.util.concurrent.locks
包下- 用于线程等待和唤醒做处理的接口,有主要两个方法
await
取代wait
signal
取代notify
二、代码实现
依照lock
执行逻辑编写即可
package com.migu;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerAndConsumer2 {
public static void main(String[] args) {
Data2 data = new Data2();
// 生产者线程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
data.increment();
}
}).start();
// 消费者线程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
data.decrement();
}
}).start();
}
}
// 数据资源
class Data2 {
private int num;
Lock lock = new ReentrantLock(); // 加锁、解锁
Condition condition = lock.newCondition(); // 等待、唤醒
public void increment() {
lock.lock();
try {
// 业务逻辑--------------
while (num != 0) {
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName() + "生产之后: " + num);
condition.signal();
//-----------------------
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() {
lock.lock();
try {
while (num == 0) {
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName() + "消费之后: " + num);
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
此时的效果的wait
和notify
是一样的,所以接下来要实现精准通知的实现
1.3、Condition的精准通知
重新理解下Condition
对象的API
- 当
Condition
调用await
方法时,是对当前持有lock
锁的这个线程进入等待队列,并释放锁 - 如果此时想要唤醒线程A,那么就需找到线程A的
Lock
锁,而这个Lock
锁就在Condition
上 - 那么也就是当
ConditionA
对线程A执行等待时,那么就要用ConditionA
去执行signal
,才可以唤醒线程A signal()
:唤醒在此Condition上的Lock对象上等待的单个线程。signalAll()
:唤醒在此Condition上的Lock对象上等待的所有线程。- 以上就是所说的精准通知
实现线程轮流执行
场景:一共三个线程分别处理不同生产、消费模块,按顺序执行
- 生产线程,每次产出数据
- 其他两个线程依次对其消费
- 注意要点:
- 就是处理好
while
语句的条件,任何一种情况都要判断到 - 对应好每个线程上
condition
持有的锁
- 就是处理好
package com.migu.procon;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerAndConsumer2 {
public static void main(String[] args) {
Data2 data = new Data2();
// 生产者线程
new Thread(() -> {
for (int i = 0; i < 2; i++) {
data.increment();
}
}).start();
// 消费者线程
new Thread(() -> {
while (true) {
data.decrement();
}
}).start();
// 消费者线程
new Thread(() -> {
while (true) {
data.decrement2();
}
}).start();
}
}
// 数据资源
class Data2 {
private int num;
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
// 生产模块
public void increment() {
lock.lock();
try {
while (num != 0) {
condition1.await();
}
num+=5; // 每次产出五份数据
System.out.println(Thread.currentThread().getName() + "生产之后: " + num);
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// 消费模块
public void decrement() {
lock.lock();
try {
// 只处理第四份和第五份数据
while (num == 0 || (num >=1 && num <=3)) {
condition2.await();
}
num--;
System.out.println(Thread.currentThread().getName() + "消费之后: " + num);
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// 消费模块
public void decrement2() {
lock.lock();
try {
while(num == 0 || (num >= 4 && num <= 5)){
condition3.await();
}
num--;
System.out.println(Thread.currentThread().getName() + "消费之后: " + num);
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
输出:
消费完毕,消费者线程进入等待,一直等待生产者线程产出数据
PS.这边例子只是为了演示顺序执行的流程,具体实际开发其实也并不清楚
还有一点:
在我们上面实现的生产、消费模式,一些列等待和通知都需要去手动实现
当数据量大的时候要去兼顾效率和安全问题,所以JUC也提供了一种方案叫做阻塞队列