Java数据结构——阻塞队列

一.什么是阻塞队列

1.常规队列中存在的问题

①在常规队列中我们想要取出一个元素,若队列为空,会返回null。如果硬要取出,只能不断循环尝试,浪费cpu的执行

②在常规队列中我们想要存入一个元素,若队列为空,会返回false。如果硬要存入,只能不断循环尝试,浪费cpu的执行

③很多场景(如等待唤醒机制)要求分离生产者和消费者,他们要由不同的线程来承担,而常规的队列并没有考虑

因此阻塞队列就是用来连接生产者与消费者之间的管道

2.认识阻塞队列

⑴概述

阻塞队列BlockingQueue是支持两个附加操作的Queue,这两个操作是:获取元素时等待队列变为非空,以及存储元素时等待空间变得可用

BlockingQueue是一个接口,该接口一共有以下8个实现类

05968a4963ab4a7ba88c021aa7a692ef.png

其中我们最常用的就是以下两种

①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接口中的抽象方法

f8dfdb6f0a7c424cb437bc73b883cc4e.png

这些方法基本与常规的队列方法一致,这里就不一一做代码演示了

二. ArrayBlockingQueue的实现

我们这里主要来代码实现一下其中最常用的入队offer以及poll出队的实现

1. BlockingQueue接口实现

①offer入队

阻塞队列中对offer的定义是:将指定的元素插入此队列的尾部,如果队列已满,则在到达指定的等待时间之前等待可用的空间

因此在我们定义的offer方法形参中要有元素(e),等待的时间(timeout)以及时间单位(unit)

因此我们可以衍生出以下两种offer方法

Ⅰ. offer(Object o)  将指定的元素插入此队列的尾部(如果立即可行且不会超过该队列的容量)

Ⅱ.offer(Object o,long timeout,TimeUnit unit)  将指定的元素插入此队列的尾部,如果队列已满,则在到达指定的等待时间之前等待可用的空间

②poll出队

 

a268d235d40e47b691607d3bc124fe33.png

2.单锁实现

首先我们要创建MyBlockingQueue的实现类MyArrayBlockingQueue,并重写其中所有的抽象方法

60ff4ac295364c3181f22a116ebccef0.png 

⑴成员变量

基于数组的阻塞队列实现

首先肯定要有一个数组array,头指针head,尾指针tail以及存入的元素长度size

其次,因为阻塞队列要维护多线程下的线程安全,因此对于其中的每一个方法,我们都需要用一把相同的锁lock来维护

ab67aecb2fe3404bbe26765cf3434137.png

dcf95dd2915848b08e5baaa99aef6573.png  

0538adf690914f6fb868b0b01640851b.png

⑵成员方法

①offer

其中立即执行的offer方法可以基于等待执行的offer方法实现

f36b5073cb7345ac8f913dd6de227bfd.png

那么我们就只需专注于等待执行的offer方法

Ⅰ.首先我们要加锁,确保方法不会并发出错

Ⅱ.其次看队列已满的情况

队列已满,我们要在规定的时间内等待(沉睡线程),若超出等待的时间,则入队失败

细节:为什么判断已满用while而不用if?

因为唤醒线程一后,若另一个线程二被先一步唤醒执行完毕,则线程一必须再次沉睡

f683637f56e040a4b51283617beef56b.png

Ⅲ.若队列未满

队列未满(可能为空)那么我们就添加元素,那么此时队列中必有元素,我们就要唤醒在poll方法中沉睡的线程

75cc2ee911794d3d928794e02b3121de.png

如图为offer方法的全部代码

1270ea8e2b534eac87359a2f933a8bf2.png

②poll

出队的逻辑与入队基本一样

首先加锁

再判断数组是否为空,为空则沉睡线程

不为空则唤醒offer中沉睡的线程

260dbb09a1fc4a67988641b00bb699ca.png

③完整的单锁代码

816f61c4c5f149eb8feea174a51bfcbe.png

⑶代码测试

下面我们可以创建两个线程,来看看阻塞队列的运行。

运行结果时设置较长时间的等待,若阻塞,会有明显的等待时间结束才能结束程序(自己运行结果看)

1ec95cc7c8cf485ab89fb78231dd4a59.jpg

3.双锁实现

在单锁实现中,因为锁只有一把,所以offer与poll不能同时运行(运行offer时,poll需在锁外等待offer执行完)

因此我们可以分别给offer与poll定义一把锁,这样入队与出队操作就可以同时运行了

⑴成员变量

成员变量有两处发生了改变

①锁变为了两把

②size要用原子变量修饰

因为两方法同时运行了,仍然存在并存现象会导致size不安全

c028652040eb4258b00d80de23fbf5c1.png 

17abf9072e444df08da7feee5b06d922.png

⑵成员方法

双锁的逻辑与单锁基本一致,唯一就是唤醒的逻辑不同

Ⅰ.首先就是唤醒的代码要定义到锁的外面,因为嵌套锁很容易导致死锁,所以我们在方法内部逻辑执行完毕后再去唤醒

Ⅱ.我们实现双锁的初衷就是为了让offer与poll可以同时运行。若我们在唤醒poll时没来的及解锁,那么poll方法就会受到影响,两方法就不能同时运行了,因此我们就要减少外面唤醒的次数,以此尽量减少对方法的影响

443b3f8c75494b73a7ab1071e51a5db9.png 

现在我们主要解决的就是唤醒操作的代码,那么如何减少外面唤醒的次数?如何由一带多呢?

①offer

首先就是让最先完成入队逻辑的offer(队列刚好由0 ->1)去唤醒poll中沉睡的线程,若仍有其余的offer完成,要去唤醒poll中沉睡的线程,那么我们就让最先完成offer的线程在唤醒poll时,再去唤醒其余的线程

在offer中最先唤醒的poll

615f8920bdf148fa8fcec54e93d34944.png

在poll中,其余要被唤醒的poll

f3877346e45d4406a1c428bf99e91769.png

②poll

poll中的唤醒逻辑与offer一致

在poll中最先唤醒的offer

7ad8f187f2484f6d8411c3b946ebd798.png

在offer中其余要被唤醒的offer

74be9e544b2d4984adf5deb0a8ae8df9.png

③完整的双锁代码

b58f81f97b574cb7b383d12768556c31.png

 

为啥idea设置背景图片后就不能用扣扣长截图了?

 

 

 

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汤姆大聪明

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值