一.什么是阻塞队列
1.常规队列中存在的问题
①在常规队列中我们想要取出一个元素,若队列为空,会返回null。如果硬要取出,只能不断循环尝试,浪费cpu的执行
②在常规队列中我们想要存入一个元素,若队列为空,会返回false。如果硬要存入,只能不断循环尝试,浪费cpu的执行
③很多场景(如等待唤醒机制)要求分离生产者和消费者,他们要由不同的线程来承担,而常规的队列并没有考虑
因此阻塞队列就是用来连接生产者与消费者之间的管道
2.认识阻塞队列
⑴概述
阻塞队列BlockingQueue是支持两个附加操作的Queue,这两个操作是:获取元素时等待队列变为非空,以及存储元素时等待空间变得可用
BlockingQueue是一个接口,该接口一共有以下8个实现类
其中我们最常用的就是以下两种
①ArrayBlockingQueue 基于数组的有界阻塞队列
BlockingQueue<E> queue=new ArrayBlockingQueue(int capacity)<>;
②LinkedBlockingQueue 基于链表的阻塞阻塞队列
BlockingQueue<E> queue=new LinkedBlockingQueue()<>;
⑵BlockingQueue的特有功能
①BlockingQueue<E>支持两个附加操作的 Queue,这两个操作是:获取元素时等待队列变为非空,以及存储元素时等待空间变得可用
②BlockingQueue 不接受 null 元素。试图 add、put 或 offer 一个 null 元素时,某些实现会抛出 NullPointerException。null 被用作指示 poll 操作失败的警戒值
③BlockingQueue 可以是限定容量(ArrayBlockingQueue)
④BlockingQueue 实现是线程安全的
⑶方法摘要
下面是BlockingQueue接口中的抽象方法
这些方法基本与常规的队列方法一致,这里就不一一做代码演示了
二. ArrayBlockingQueue的实现
我们这里主要来代码实现一下其中最常用的入队offer以及poll出队的实现
1. BlockingQueue接口实现
①offer入队
阻塞队列中对offer的定义是:将指定的元素插入此队列的尾部,如果队列已满,则在到达指定的等待时间之前等待可用的空间
因此在我们定义的offer方法形参中要有元素(e),等待的时间(timeout)以及时间单位(unit)
因此我们可以衍生出以下两种offer方法
Ⅰ. offer(Object o) 将指定的元素插入此队列的尾部(如果立即可行且不会超过该队列的容量)
Ⅱ.offer(Object o,long timeout,TimeUnit unit) 将指定的元素插入此队列的尾部,如果队列已满,则在到达指定的等待时间之前等待可用的空间
②poll出队
2.单锁实现
首先我们要创建MyBlockingQueue的实现类MyArrayBlockingQueue,并重写其中所有的抽象方法
⑴成员变量
基于数组的阻塞队列实现
首先肯定要有一个数组array,头指针head,尾指针tail以及存入的元素长度size
其次,因为阻塞队列要维护多线程下的线程安全,因此对于其中的每一个方法,我们都需要用一把相同的锁lock来维护
⑵成员方法
①offer
其中立即执行的offer方法可以基于等待执行的offer方法实现
那么我们就只需专注于等待执行的offer方法
Ⅰ.首先我们要加锁,确保方法不会并发出错
Ⅱ.其次看队列已满的情况
队列已满,我们要在规定的时间内等待(沉睡线程),若超出等待的时间,则入队失败
细节:为什么判断已满用while而不用if?
因为唤醒线程一后,若另一个线程二被先一步唤醒执行完毕,则线程一必须再次沉睡
Ⅲ.若队列未满
队列未满(可能为空)那么我们就添加元素,那么此时队列中必有元素,我们就要唤醒在poll方法中沉睡的线程
如图为offer方法的全部代码
②poll
出队的逻辑与入队基本一样
首先加锁
再判断数组是否为空,为空则沉睡线程
不为空则唤醒offer中沉睡的线程
③完整的单锁代码
⑶代码测试
下面我们可以创建两个线程,来看看阻塞队列的运行。
运行结果时设置较长时间的等待,若阻塞,会有明显的等待时间结束才能结束程序(自己运行结果看)
3.双锁实现
在单锁实现中,因为锁只有一把,所以offer与poll不能同时运行(运行offer时,poll需在锁外等待offer执行完)
因此我们可以分别给offer与poll定义一把锁,这样入队与出队操作就可以同时运行了
⑴成员变量
成员变量有两处发生了改变
①锁变为了两把
②size要用原子变量修饰
因为两方法同时运行了,仍然存在并存现象会导致size不安全
⑵成员方法
双锁的逻辑与单锁基本一致,唯一就是唤醒的逻辑不同
Ⅰ.首先就是唤醒的代码要定义到锁的外面,因为嵌套锁很容易导致死锁,所以我们在方法内部逻辑执行完毕后再去唤醒
Ⅱ.我们实现双锁的初衷就是为了让offer与poll可以同时运行。若我们在唤醒poll时没来的及解锁,那么poll方法就会受到影响,两方法就不能同时运行了,因此我们就要减少外面唤醒的次数,以此尽量减少对方法的影响
现在我们主要解决的就是唤醒操作的代码,那么如何减少外面唤醒的次数?如何由一带多呢?
①offer
首先就是让最先完成入队逻辑的offer(队列刚好由0 ->1)去唤醒poll中沉睡的线程,若仍有其余的offer完成,要去唤醒poll中沉睡的线程,那么我们就让最先完成offer的线程在唤醒poll时,再去唤醒其余的线程
在offer中最先唤醒的poll
在poll中,其余要被唤醒的poll
②poll
poll中的唤醒逻辑与offer一致
在poll中最先唤醒的offer
在offer中其余要被唤醒的offer
③完整的双锁代码
为啥idea设置背景图片后就不能用扣扣长截图了?