在学习数据结构的时候,我们学习了队列以及优先级队列。现在,我们来学习一下阻塞队列。

阻塞队列,是一种更复杂的队列

1 特性

1) 线程安全

2) 阻塞特性

   a)队列为空,尝试出队列,出队列操作就会阻塞,阻塞到其他线程添加元素为止

   b)队列为满,尝试入队列,入队列操作也会阻塞,阻塞到其他线程取走元素为止

2 应用场景—生产者消费者模型

举个生活中的例子:

小蓝、小红、小绿他们三个一起包饺子,小蓝负责擀饺子皮,小蓝擀的饺子皮会放在盖帘上,小红和小绿就可以在盖帘上拿饺子皮包饺子。

其中,资源:饺子皮   生产者:小蓝   

消费者:小红和小绿    生产消费的交易场所:盖帘

此处的交易场所就是一个阻塞队列。

2.1 生产者消费者模型的两个重要优势(阻塞队列)

1)解耦合(不一定是两个线程之间,也可以是两个服务器之间)

如果A直接访问B,此时A和B的耦合就更高

编写A代码的时候,多多少少会有一些和B相关的逻辑,同样编写B的代码的时候,多多少少会有一些和A相关的逻辑

A和队列交互,B也和队列交互,A和B就不再直接交互了

A的代码中看不到B,B的代码中看不到A,A的代码中和B的代码中只能看到队列了

本来`是A和B耦合,现在成了A和队列耦合,B和队列耦合。这时候,大家可能就有疑问了:那不还是耦合吗?也没降低耦合呀??

降低耦合,是为了让后续修改的时候,成本低,对于队列来说呢,一般就不会修改~(之前A B耦合带来的问题,A修改的时候,B也得同时修改)

2)削峰填谷

补充:服务器挂了:服务器处理每个请求时,都会消耗一定的硬件资源(包括但不限于CPU、内存、硬盘、网络带宽……),要是同时有n个请求,消耗的量就得*n,一旦消耗的总量超过了机器硬件资源的上限,此时,对应的线程可能就会崩溃或者操作系统发生卡顿。

问:那么为什么B会挂,但A不挂呢???

答:一般来说,像A这种上游的服务器,尤其是入口的服务器,单个请i求消耗的资源数少(干的活简单),对于 B这种下游的服务器,通常会承担更重的任务(复杂的计算/存储),单个请求消耗的资源数多。

日常生活中,会给B这样的服务器分配更好的机器。即使如此,也很难保证B承担的访问量能够比A更高

所以:

峰,一般是突发的,时间短。趁着峰值过去了,B仍然可以利用波谷的时间处理之前积压的数据,所以,就叫做:削峰填谷

但是,事物具有两面性,有利就会有弊

2.2 生产者消费模型付出的代价

1)引入队列之后,数据的结构会更复杂(需要更多的机器进行部署,生产环境的结构会更复杂,管理起来更加麻烦)

2)效率会受到影响

3. 阻塞队列

3.1 基本使用

Java标准库中,提供了现成的阻塞队列:BlockingQueue

我们用put表示入队列,take表示出队列,当然,offer和poll也可以使用,但是,只有put和take有阻塞功能。

代码示例:

运行结果:

同时别忘了处理异常

如果阻塞队列满了,要是还想往队列里面放入元素,就会阻塞:

代码示例:

运行结果:

capacity为最大容量,最多能容纳多少元素

打开jconsoler,你就会看到线程的状态为waiting

如果不设置capacity,默认是一个非常大的数值……,在实际开发中,一般建议大家能够设置要求的最大值。否则你的队列会非常大,导致内存耗尽

值得注意的是,阻塞队列没有提供一个”阻塞的获取队首元素的操作“

3.2 简单实现生产者消费者模型

代码示例:

运行结果:

我们可以看到,直接运行的话,生产者和消费者两个线程的速度旗鼓相当,很难看到阻塞的效果

所以让其中一个线程跑的慢一点,看到阻塞效果

1)在生产者中加sleep(让生产元素慢一点,每次生产完一个元素,sleep)

看到的是队列空

运行结果:

消费跟着生产走,消费会因为生产的速度产生一个阻塞的效果(一定不要忘了阻塞队列的特性)

2)在消费者中加sleep(生产快,消费慢,会看到队列满的情况)

运行结果:

生产会跟着消费走

以上两种情况,都是极端情况,正常情况下是不会阻塞的(最开始的情况:生产一会,消费一会,两者转的很快),要是阻塞的话,快的就得跟着慢的,保持步调一致

4 模拟实现一个简单的阻塞队列

4.1 前言

既然要模拟阻塞队列,就得满足阻塞队列的特性,先来满足第一个线程安全的特性

代码示例:

当然也可以不用this,新建一个锁对象啥的也可以

接下来,就得满足阻塞队列的第二个很长的特性了,就需要使用“wait 和 join了”

对于入队,代码示例:

所以:

对于出队,代码示例:

所以:

假设有若干个线程使用这个队列,要么所有的线程阻塞在put,要么所有的线程阻塞在take,不可能有一些阻塞在put,有一些阻塞在take,因为队列不可能既是满的,又是空的

除了wait和notify,还有一个关键环节:
那就是把if换成while

原因:

 这里的wait是确保接下来的操作有意义。此处要求,执行到下面逻辑,size必须不能为0。正常来说,wait的唤醒,是通过另一个线程执行put,put执行成功了,size不可能为0.

 但是wait不一定被notify给唤醒,还可能被interupt给打断,执行到下面的操作就完了

但是如果把if换成while,就可以进行二次(有可能多次)判断,判断这里的条件是否成立:wait之前判定一次,wait唤醒也判定一次(再确认一下队列是否为空)。

这就好比,你有一个非常喜欢的东西,但是你的妈妈不同意给你买,终于有一天,她同意给你买了,你太开心了,害怕她是逗你玩,你就又去确认一下她是否真的同意给你买。

使用while,还可以避免“同类”唤醒“同类”,put唤醒其他线程的put(其他线程已经put阻塞了,这个时候又来一个新的put,也是在条件阻塞,队列已经满了),这种极端的情况,比如: 3个线程(1 2 3)put都因为队列满阻塞了,这个时候第4个线程take了一下,唤醒了1,1继续往下执行(这时候队列又满了): 确实会触发notify,,此时有可能唤醒2、3这俩个线程其中一个的,但是因为是while,即使wait被唤醒,还会再次判定条件,再次阻塞

4.2 代码


5 基于4的代码简单实现生产者消费者模型

代码示例:

运行结果和3是一样的,要想加入sleep看阻塞效果啥的,和3的代码也是一样的。

补充一点:要是把capacity设为0的话,可就添加不了元素了,就不是队列喽~

OK啦,到此结束!!!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐