1. 什么是生产者/消费者模型
一种重要的模型,基于等待/通知机制。生产者/消费者模型描述的是有一块缓冲区作为仓库,生产者可将产品放入仓库,消费者可以从仓库中取出产品,生产者/消费者模型关注的是以下几个点:
- 生产者生产的时候消费者不能消费
- 消费者消费的时候生产者不能生产
- 缓冲区空时消费者不能消费
- 缓冲区满时生产者不能生产
1.1 生产者消费者模型的优点:
- 解耦。
因为多了一个缓冲区,所以生产者和消费者并不直接相互调用,这一点很容易想到,这样生产者和消费者的代码发生变化,都不会对对方产生影响,这样其实就把生产者和消费者之间的强耦合解开,变为了生产者和缓冲区/消费者和缓冲区之间的弱耦合 - 通过平衡生产者和消费者的处理能力来提高整体处理数据的速度,这是生产者/消费者模型最重要的一个优点。
如果消费者直接从生产者这里拿数据,如果生产者生产的速度很慢,但消费者消费的速度很快,那消费者就得占用CPU的时间片白白等在那边。有了生产者/消费者模型,生产者和消费者就是两个独立的并发体,生产者把生产出来的数据往缓冲区一丢就好了,不必管消费者;消费者也是,从缓冲区去拿数据就好了,也不必管生产者,缓冲区满了就不生产,缓冲区空了就不消费,使生产者/消费者的处理能力达到一个动态的平衡
2. 利用wait/notify实现:
需求:生产一个、消费一个
/**
* 生产者消费者模型三段式:
* 1. 判断等待
* 2. 执行的业务
* 3. 通知
*/
public class WithWaitAndNotify {
public static void main(String[] args) {
ProdAndCons prodAndCons = new ProdAndCons();
// 生产线程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
prodAndCons.increment();
}
}).start();
// 消费线程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
prodAndCons.decrement();
}
}).start();
}
}
class ProdAndCons {
// 相当于一个缓冲区
private int number = 0;
//生产者
public synchronized void increment() {
// 1.判断等待
if (number != 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 2.业务
number++;
System.out.println(Thread.currentThread().getName() + "生产了商品,数量:" + number);
// 3.通知:通知其他线程,我生产了商品
this.notifyAll();
}
// 消费者
public synchronized void decrement() {
// 1.判断等待
if (number == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 2.业务
number--;
System.out.println(Thread.currentThread().getName() + "消费了商品,数量:" + number);
// 3.通知:通知其他线程,我消费了商品,请生产
this.notifyAll();
}
}
查看结果:
Thread-0生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-0生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-0生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-0生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-0生产了商品,数量:1
Thread-1消费了商品,数量:0
2.1 问题:
此时我只开了2个线程,假如线程很多呢?
再添加2个或三个生产者消费者线程,这里不展示代码了,直接上结果:
Thread-0生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-2生产了商品,数量:1
Thread-0生产了商品,数量:2
Thread-2生产了商品,数量:3
Thread-0生产了商品,数量:4
Thread-2生产了商品,数量:5
Thread-0生产了商品,数量:6
Thread-2生产了商品,数量:7
Thread-0生产了商品,数量:8
Thread-2生产了商品,数量:9
Thread-3消费了商品,数量:8
Thread-3消费了商品,数量:7
Thread-3消费了商品,数量:6
Thread-3消费了商品,数量:5
Thread-3消费了商品,数量:4
Thread-1消费了商品,数量:3
Thread-1消费了商品,数量:2
Thread-1消费了商品,数量:1
Thread-1消费了商品,数量:0
大家可以看到,结果很惊人,甚至出现了10件商品,按道理,假如已经生产了一个商品后,就应该wait等待了,可是好像并没有wait等待。这是由于发生了虚假唤醒:
2.2 虚假唤醒:
先来解释下上述问题出现的原因,我们通过结果来假设情况:
Thread-0生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-2生产了商品,数量:1
Thread-0生产了商品,数量:2
。。。。
- 线程0生产了一个商品,唤醒所有线程,随后再次抢占到资源,进入if判断条件为真,进入等待状态
- 线程2抢占到资源,进入判断条件为真,进入到if条件中的等待状态
- 线程1抢占到资源消费了一个商品,随机唤醒一个等待的线程,唤醒了线程2
- 线程2继续之前刚刚未完成的任务,生产了一个商品,随机唤醒一个等待的线程,唤醒了线程0
- 线程0继续执行刚刚未完成的任务,生产了一个商品,此时商品数量为2
上述情况就是虚假唤醒的原因,那么我们如何避免上述情况的发生呢?
我们可以把if改成while,让他进入循环,不停的判断是否已经生产或者已经消费了。查看结果:
Thread-0生产了商品,数量:1
Thread-3消费了商品,数量:0
Thread-2生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-2生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-2生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-2生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-2生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-0生产了商品,数量:1
Thread-3消费了商品,数量:0
Thread-0生产了商品,数量:1
Thread-3消费了商品,数量:0
Thread-0生产了商品,数量:1
Thread-3消费了商品,数量:0
Thread-0生产了商品,数量:1
Thread-3消费了商品,数量:0
可以看到,是我们想要的结果。
查看API官方文档:
3. 利用Lock及codition实现:
代码:
/**
* 生产者消费者模型三段式:
* 1. 判断等待
* 2. 执行的业务
* 3. 通知
*/
public class WithLockAndCondition {
public static void main(String[] args) {
ProdAndCons2 prodAndCons = new ProdAndCons2();
// 生产线程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
prodAndCons.increment();
}
}).start();
// 消费线程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
prodAndCons.decrement();
}
}).start();
// 生产线程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
prodAndCons.increment();
}
}).start();
// 消费线程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
prodAndCons.decrement();
}
}).start();
}
}
class ProdAndCons2 {
// 相当于一个缓冲区
private int number = 0;
Lock lock = new ReentrantLock();
// 获取锁的对象监视器
Condition condition = lock.newCondition();
// 生产者
public void increment() {
try {
lock.lock();
// 1.判断等待
while (number > 0) {
condition.await();
}
// 2.业务
number++;
System.out.println(Thread.currentThread().getName() + "生产了商品,数量:" + number);
// 3.通知:通知其他线程,我生产了商品
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// 消费者
public void decrement() {
try {
lock.lock();
// 1.判断等待
while (number == 0) {
condition.await();
}
// 2.业务
number--;
System.out.println(Thread.currentThread().getName() + "消费了商品,数量:" + number);
// 3.通知:通知其他线程,我生产了商品
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
结果:
Thread-0生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-2生产了商品,数量:1
Thread-3消费了商品,数量:0
Thread-0生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-2生产了商品,数量:1
Thread-3消费了商品,数量:0
Thread-0生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-2生产了商品,数量:1
Thread-3消费了商品,数量:0
Thread-0生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-2生产了商品,数量:1
Thread-3消费了商品,数量:0
Thread-0生产了商品,数量:1
Thread-1消费了商品,数量:0
Thread-2生产了商品,数量:1
Thread-3消费了商品,数量:0
3.1 Condition实现精准通知
代码:
public class ConditionChoose {
public static void main(String[] args) {
ProdAndCons3 prodAndCons3 = new ProdAndCons3();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
prodAndCons3.printA();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
prodAndCons3.printB();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
prodAndCons3.printC();
}
}, "C").start();
}
}
class ProdAndCons3 {
private final Lock lock = new ReentrantLock();
private final Condition condition1 = lock.newCondition();
private final Condition condition2 = lock.newCondition();
private final Condition condition3 = lock.newCondition();
private int number = 1;
public void printA() {
try {
lock.lock();
// 业务, 判断 -> 执行 -> 通知
while (number != 1) {
// 等待
condition1.await();
}
System.out.println(Thread.currentThread().getName() + "=>AAA");
// 唤醒指定的人B:
number = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
try {
lock.lock();
// 业务, 判断 -> 执行 -> 通知
while (number != 2) {
// 等待
condition2.await();
}
System.out.println(Thread.currentThread().getName() + "=>BBB");
// 唤醒指定的人C:
number = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
try {
lock.lock();
// 业务, 判断 -> 执行 -> 通知
while (number != 3) {
// 等待
condition3.await();
}
System.out.println(Thread.currentThread().getName() + "=>CCC");
// 唤醒指定的人A:
number = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
结果:
A=>AAA
B=>BBB
C=>CCC
A=>AAA
B=>BBB
C=>CCC
A=>AAA
B=>BBB
C=>CCC
创建多个conditon对象,每个condition对象控制一个线程的唤醒与等待。