欢迎来到《并发王者课》,阻塞队列本文是课铂该系列文章中的第18篇。
在线程的金致竟何同步中,阻塞队列是胜良一个绕不过去的话题,它是器无同步器底层的关键 。所以,面目我们在本文中将为你介绍阻塞队列的阻塞队列基本原理 ,以了解它的课铂工作机制和它在Java中的实现 。本文稍微有点长 ,金致竟何建议先了解大纲再细看章节 。胜良
在生活中,器无相信你一定见过下图的面目人山人海 ,也见过其中的阻塞队列秩序井然 。混乱 ,课铂是金致竟何失控的开始。想想看 ,在没有秩序的情况下 ,拥挤的人流蜂拥而上十分危险 ,轻则挤出一身臭汗 ,重则造成踩踏事故。而秩序,则让情况免于混乱,排好队大家都舒服。
面对人流,我们通过排队解决混乱 。而面对多线程 ,我们也通过队列让线程间免于混乱 ,这就是阻塞队列为何而存在。
所谓阻塞队列,你可以理解它是这样的一种队列 :
下面这张图展示了多线程是如何通过阻塞队列进行协作的:
从图中可以看到,对于阻塞队列数据的读写并不局限于单个线程 ,往往存在多个线程的竞争。
接下来我们先抛开JUC中复杂的阻塞队列,来设计一个简单的阻塞队列,以了解它的核心思想 。
在下面的阻塞队列中,我们设计一个队列 ,并通过字段限定它的容量 。方法用于向队列中放入数据 ,如果队列已满则等待;而方法则用于从数据中取出数据 ,如果队列为空则等待 。
定义线程向队列中放入数据,线程从队列中取出数据 。
运行结果如下 :
从结果中可以看到,设计的阻塞队列已经可以有效工作,你可以仔细地品一品输出的结果 。当然,这个阻塞是极其简单的,在下面一节中,我们将介绍Java中的阻塞队列设计。
Java中的阻塞队列有两个核心接口 :BlockingQueue和BlockingDeque,相关的接口实现设继承关系如下图所示 。相比于上一节中我们自定义的阻塞队列 ,Java中的实现要复杂很多。不过 ,你不必为此担心 ,理解阻塞队列最重要的是理解它的思想和实现的思路,况且Java中的实现其实很有意思 ,读起来也比较轻松。
从图中可以看出,BlockingQueue接口继承了Queue接口和Collection接口 ,并有LinkedBlockingQueue和ArrayBlockingQueue两种实现 。这里有个有意思的地方,继承Queue接口很容易理解,可以为什么要继承Collection接口 ?先卖个关子,你可以思考一会,稍后会给出答案。
BlockingQueue中义了关于阻塞队列所需要的一系列方法,它们彼此之间看起来很像 ,从表面上看不出明显的差别。对于这些方法 ,你不必死记硬背,下图的表格中将这些方法分为了A 、B 、C 、D这四种类型 ,分类之后再去理解它们会容易很多:
其中部分关键方法的解释如下:
将这些方法填入前面的那张图,它应该长这样:
LinkedBlockingQueue实现了BlockingQueue接口,遵从先进先出(FIFO)的原则 ,提供了可选的有界阻塞队列( Optionally Bounded )的能力 ,并且是线程安全的 。
LinkedBlockingQueue的数据结构并不复杂,不过需要注意的是,数据结构中并不包含List,仅有和两个Node ,设计上比较巧妙 。
注意 ,LinkedBlockingQueue有两把锁 ,读取和写入的锁是分离的 !这和下面的ArrayBlockingQueue并不相同。
下面截取了LinkedBlockingQueue中读写的部分代码 ,值得你仔细品一品。品的时候 ,要重点关注两把锁的使用和读写时数据结构是如何变化的 。
最后说下LinkedBlockingQueue为什么要继承Collection接口 。我们知道,Collection接口有这样的移除方法,而这些方法在队列中也是有使用场景的。比如,你把一个数据错误地放入了队列 ,或者你需要移除已经失效的数据,那么Collection的一些方法就派上了用场。
ArrayBlockingQueue是BlockingQueue接口的另外一种实现,它与LinkedBlockingQueue在设计目标上的的关键不同,在于它是有界的 。
核心数据结构
从数据结构中可以看出,ArrayBlockingQueue使用的是数组 ,而数组是有界的 。
核心构造
线程安全性
在读写锁方面 ,前面已经说过,LinkedBlockingQueue和ArrayBlockingQueue是不同的,ArrayBlockingQueue只有一把锁,读写用的都是它 。
下面截取了ArrayBlockingQueue中读写的部分代码,值得你仔细品一品。品的时候,要重点关注读写锁的使用和读写时数据结构是如何变化的。
在Java中,BlockingDeque与BlockingQueue是一对孪生兄弟似的存在,它们长得实在太像了,不注意的话很容易混淆。
但是,BlockingDeque与BlockingQueue核心不同在于 ,BlockingQueue只能够从尾部写入 、从头部读取,使用上很有限制。而BlockingDeque则支持从任意端读写 ,在读写时可以指定头部和尾部 ,丰富了阻塞队列的使用场景。
相较于BlockingQueue ,BlockingDeque的方法显然要更丰富一些,毕竟它支持了双端的读写。但是,丰富归丰富,在类型上仍然和BlockingQueue是一致的 ,你仍然可以参考上面的A、B、C、D四种类型来分类理解。为了节约篇幅,我们这里就不再罗列,只选取了其中的部分方法作了解释: