1. 概述
生产者-消费者是一个经典的多线程协作问题。所谓生产者-消费者问题,实际上是包含两类线程,一种是生产者线程,用于生产数据,另一种是消费者线程,用于消费数据。为了解耦生产者和消费者的关系,通常会采用共享的数据区域。生产者往共享区域放数据,无需关心消费者的行为。消费者从共享区域取数据,无需关心生产者的行为。同时,生产者和消费者之间应该具备以下功能:
- 如果共享数据区已满,阻塞生产者继续生产数据到共享数据区;
- 如果共享数据区为空,阻塞消费者线程消费共享数据区的数据;
接下来,我们介绍几种方法来完成生产者-消费者模型。
2. wait/notifyAll模型
wait和notify方法是Object类自带的方法,用于线程之间的阻塞和唤醒。我们通过下面的代码来具体看看wait/notify模型如何使用:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestProducerAndConsumer1 {
public static void main(String[] args) {
List<Integer> storage = new ArrayList<Integer>();
ExecutorService producerExecutorService = Executors.newFixedThreadPool(2);
ExecutorService consumerExecutorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
producerExecutorService.execute(new Producer(storage, 1)); // 生产者
}
for (int i = 0; i < 5; i++) {
consumerExecutorService.execute(new Consumer(storage)); // 消费者
}
}
static class Producer extends Thread {
List<Integer> storage; // 仓库
Random random;
int maxSize; // 最大容量
public Producer(List<Integer> storage, int maxSize) {
this.storage = storage;
random = new Random();
this.maxSize = maxSize;
}
@Override
public void run() {
synchronized (storage) {
try {
while (storage.size() == maxSize) { // 达到最大容量,生产者进入等待
System.out.println("库存满了,生产者等待消耗商品");
storage.wait();
}
int randNum = random.nextInt(1000);
storage.add(randNum);
System.out.println("生产者生产商品:" + randNum);
storage.notifyAll(); // 唤醒等待的消费者
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
static class Consumer extends Thread {
List<Integer> storage;
public Consumer(List<Integer> storage) {
this.storage = storage;
}
@Override
public void run() {
synchronized (storage) {
try {
while (storage.isEmpty()) { // 库存空了,消费者进入等待
System.out.println("库存空了,消费者等待补充商品");
storage.wait();
}
int randNum = storage.remove(0);
System.out.println("消费者消费商品" + randNum);
storage.notifyAll(); // 唤醒等待的生产者
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
生产者生产商品:634
库存满了,生产者等待消耗商品
库存满了,生产者等待消耗商品
消费者消费商品634
生产者生产商品:689
库存满了,生产者等待消耗商品
消费者消费商品689
生产者生产商品:448
库存满了,生产者等待消耗商品
消费者消费商品448
生产者生产商品:957
库存满了,生产者等待消耗商品
消费者消费商品957
生产者生产商品:939
库存满了,生产者等待消耗商品
消费者消费商品939
生产者生产商品:225
库存满了,生产者等待消耗商品
库存满了,生产者等待消耗商品
通过上面的代码我们实现了生产者-消费者模型,也完成了必须具备的两个功能。但是有一点问题需要注意下:在等待队列中,排在队首的可能是生产者,也可能是消费者。如果使用notify方法来唤醒,就有可能出现生产者只唤醒生产者,消费者只唤醒消费者,这就会出现问题。所以我们在代码中使用notifyAll方法+业务逻辑判断地方式唤醒所有线程,然后利用while循环来判断是否真的符合解除阻塞的条件。
3. Lock中Condition的await/signalAll模型
我们以ReentrantLock中的Condition条件队列来举例await/signalAll模型:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class TestProducerAndConsumer2 {
public static void main(String[] args) {
List<Integer> storage = new ArrayList<Integer>();
ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition(); // condition条件队列
ExecutorService producerExecutorService = Executors.newFixedThreadPool(2);
ExecutorService consumerExecutorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
producerExecutorService.execute(new Producer(storage, reentrantLock, condition,2)); // 生产者线程
}
for (int i = 0; i < 5; i++) {
consumerExecutorService.execute(new Consumer(storage, reentrantLock, condition)); // 消费者线程
}
}
static class Producer extends Thread {
List<Integer> storage;
Random random;
ReentrantLock reentrantLock;
Condition condition;
int maxSize;
public Producer(List<Integer> storage, ReentrantLock reentrantLock, Condition condition,int maxSize) {
this.reentrantLock = reentrantLock;
this.storage = storage;
this.condition = condition;
this.maxSize = maxSize; // 仓库最大容量
random = new Random();
}
@Override
public void run() {
reentrantLock.lock();
try {
while(storage.size()==maxSize){
System.out.println("库存满了,生产者等待消耗商品");
condition.await(); // 仓库满了,生产者进入等待
}
int randNum = random.nextInt(1000);
storage.add(randNum);
System.out.println("生产者生产商品:" + randNum);
condition.signalAll(); // 仓库补货了,唤醒消费者
}catch (Exception e){
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
}
static class Consumer extends Thread {
ReentrantLock reentrantLock;
Condition condition;
List<Integer> storage;
public Consumer(List<Integer> storage, ReentrantLock reentrantLock, Condition condition) {
this.reentrantLock = reentrantLock;
this.storage = storage;
this.condition = condition;
}
@Override
public void run() {
reentrantLock.lock();
try {
while (storage.isEmpty()) {
System.out.println("库存空了,消费者等待补充商品");
condition.await(); // 仓库空了,消费者进入等待
}
int randNum = storage.remove(0);
System.out.println("消费者消费商品" + randNum);
condition.signalAll(); // 消费商品了,唤醒生产者
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
reentrantLock.unlock();
}
}
}
}
输出结果:
生产者生产商品:340
生产者生产商品:491
库存满了,生产者等待消耗商品
库存满了,生产者等待消耗商品
消费者消费商品340
消费者消费商品491
库存空了,消费者等待补充商品
生产者生产商品:888
生产者生产商品:250
库存满了,生产者等待消耗商品
消费者消费商品888
消费者消费商品250
生产者生产商品:357
生产者生产商品:135
库存满了,生产者等待消耗商品
库存满了,生产者等待消耗商品
消费者消费商品357
生产者生产商品:227
库存满了,生产者等待消耗商品
库存满了,生产者等待消耗商品
实际上await/signalAll模型和wait/notifyAll模型是很相似的,主要是await/signalAll模型需要借助于Lock。同样在这里我们采用了signalAll方法,没有采用signal方法。
4. BlockingQueue模型
这里我们利用的是阻塞队列的方式,分别采用put()和take()方法,当队列满或者队列空的时候,生产者和消费者进程就会被阻塞。如下面的例子所示:
import java.util.Random;
import java.util.concurrent.*;
public class TestProducerAndConsumer3 {
public static void main(String[] args) {
BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque(2); // 定义仓库容量为2
ExecutorService producerExecutorService = Executors.newFixedThreadPool(2);
ExecutorService consumerExecutorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
producerExecutorService.execute(new Producer(blockingDeque)); // 生产者
}
for (int i = 0; i < 5; i++) {
consumerExecutorService.execute(new Consumer(blockingDeque)); // 消费者
}
}
static class Producer extends Thread {
BlockingDeque<Integer> blockingDeque;
Random random;
public Producer(BlockingDeque blockingDeque) {
this.blockingDeque = blockingDeque;
random = new Random();
}
@Override
public void run() {
try {
int randNum = random.nextInt(1000);
blockingDeque.put(randNum); //生产商品
System.out.println("生产者生产商品:" + randNum);
Thread.sleep(500);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
static class Consumer extends Thread {
BlockingDeque<Integer> blockingDeque;
public Consumer(BlockingDeque<Integer> blockingDeque) {
this.blockingDeque = blockingDeque;
}
@Override
public void run() {
try {
int randNum = blockingDeque.take(); // 消费商品
System.out.println("消费者消费商品" + randNum);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
输出结果:
生产者生产商品:904
生产者生产商品:28
消费者消费商品904
消费者消费商品28
生产者生产商品:298
消费者消费商品160
消费者消费商品298
生产者生产商品:160
生产者生产商品:22
消费者消费商品22
生产者生产商品:102
生产者生产商品:561
采用BlockingQueue的好处在于不需要我们自己定义判断仓库满或空的逻辑,BlockingQueue内部会自己实现好,简化了我们的工作。
参考文章:
一篇文章,让你彻底弄懂生产者–消费者问题