生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
它的模型和下图一样,生产者生产资源,放置共享资源里面,共享资源就类似于一个仓库,用于存储资源,消费者消费仓库中的资源,同时可能会有多个生产者与多个消费者。
首先定义BlockingQueue类,这个类就相当于仓库,用队列来存储数据。
//存取数据
private final LinkedList<E> queue = new LinkedList<E>();
//有界队列最大值
private int max;
private static final int DEFAULT_MAX = 10;
public BlockingQueue(){
this(DEFAULT_MAX);
}
public BlockingQueue(int max){
this.max = max;
}
生产者生产动作用一个put函数去实现,它的功能就是生产数据:
- 首先判断当前仓库(队列表示)是否已满,如果满了就阻塞生产线程,停止生产动作;
- 如果未满,就利用addLast模拟数据生产过程,将数据value添加至队列尾,也就是存储于仓库中;
- 最后唤醒所有线程,让它们争抢资源,有可能唤醒的是生产者,也有可能唤醒的是消费者;
//生产数据
public void put(E value){
//生产数据
//多个生产者对应多个消费者,均对queue进行操作,通过同步机制保护线程安全
synchronized(queue){
//队列已满,阻塞生产者
while(queue.size() >= max){
System.out.println(Thread.currentThread().getName()+":queue is full");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//反之,生产者生产数据
System.out.println(Thread.currentThread().getName()+":the new data has been produced");
queue.addLast(value);
//期望唤醒消费者线程,进行消费
queue.notifyAll();
}
}
消费者的消费动作用一个take函数去实现,它的功能就是消费生产者生产的数据:
- 首先判断当前仓库(队列表示)是否已空,如果空了就阻塞消费线程,停止消费动作;
- 如果未空,就利用removeFirst模拟数据消费过程,将第一个数据取出赋值给result;
- 最后唤醒所有线程,让它们争抢资源,有可能唤醒的是生产者,也有可能唤醒的是消费者,并返回result;
//消费数据
public E take(){
//消费数据
synchronized (queue){
//队列已空,不允许消费数据,阻塞消费者
while(queue.isEmpty()){
System.out.println(Thread.currentThread().getName()+":the queue is empty");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//反之,消费者消费数据
E result = queue.removeFirst();
//期望唤醒生产者线程,进行生产
queue.notifyAll();
System.out.println(Thread.currentThread().getName()+":the data "+result+" has taken");
return result;
}
}
接下来设立测试函数,通过两个线程来生产消费。生产者线程生产1-1000的数据,消费者线程每消费一次,休眠100ms,因为生产者生产需要时间,需要一定的缓冲。
public class ProdecerAndConsumerTest {
public static void main(String[] args) {
BlockingQueue<Integer> blockingQueue = new BlockingQueue<>();
new Thread("Producer"+i){
@Override
public void run() {
while(true){
blockingQueue.put((int)(1+Math.random()*1000));
}
}
}.start();
//消费者消费数据
new Thread("Consumer"+i) {
@Override
public void run() {
while (true) {
blockingQueue.take();
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
运行过程如下:仓库最大值存放10个数据,第一行可以看出此时仓库已满,接下来消费者线程消费了一个数据,仓库数量变成9,;再下来生产者抢到资源,此时数量为9,小于最大值10,继续生产了一个数据;第7行当仓库已满,生产者又抢到资源后,就阻塞掉生产线程。
此项测试为1个生产者,1个消费者,接下来测试多个生产者对多个消费者:
public class ProdecerAndConsumerTest {
public static void main(String[] args) {
BlockingQueue<Integer> blockingQueue = new BlockingQueue<>();
//生产者消费数据
for (int i = 0; i < 3; i++) {
new Thread("Producer"+i){
@Override
public void run() {
while(true){
blockingQueue.put((int)(1+Math.random()*1000));
}
}
}.start();
}
//消费者消费数据
for (int i = 0; i < 3; i++) {
new Thread("Consumer"+i) {
@Override
public void run() {
while (true) {
blockingQueue.take();
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
}
测试结果如下:3个生产者,3个消费者,随机的公平的竞争monitor lock,获得锁的就执行生产消费操作:
关于put方法中为什么用while不用if:
由于if只能判断一次,因此会导致两个问题:
1) queue中没有元素仍然调用removeFirst
比如:threadA、threadB在执行take方法都陷入了阻塞,另外一个threadC在执行put之后唤醒其中一个在take方法中阻塞的threadA,threadA消费完数据之后唤醒threadB,这就导致queue中没有元素threadB仍然调用removeFirst方法
2) queue中元素超过给定的阀值仍然会执行addLast
比如:threadA、threadB在执行put方法都陷入了阻塞,另外一个threadC在执行take之后唤醒其中一个在put方法中阻塞的threadA, threadA生产完数据之后唤醒threadB,这就导致绕开的阀值检查,调用addLast继续向队列中添加元素