首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

阻塞队列(3)LinkedBlockingQueue 源码详解

问题

输入图片说明

LinkedBlockingQueue是什么?

优缺点?

应用场景?

源码实现?

个人启发?

LinkedBlockingQueue

双向并发阻塞队列。

所谓双向是指可以从队列的头和尾同时操作,并发只是线程安全的实现,阻塞允许在入队出队不满足条件时挂起线程,这里说的队列是指支持FIFO/FILO实现的链表。

要想支持阻塞功能,队列的容量一定是固定的,否则无法在入队的时候挂起线程。也就是capacity是final类型的。

既然是双向链表,每一个结点就需要前后两个引用,这样才能将所有元素串联起来,支持双向遍历。也即需要prev/next两个引用。

双向链表需要头尾同时操作,所以需要first/last两个节点,当然可以参考LinkedList那样采用一个节点的双向来完成,那样实现起来就稍微麻烦点。

既然要支持阻塞功能,就需要锁和条件变量来挂起线程。这里使用一个锁两个条件变量来完成此功能。

优缺点

优点当然是功能足够强大,同时由于采用一个独占锁,因此实现起来也比较简单。所有对队列的操作都加锁就可以完成。同时独占锁也能够很好的支持双向阻塞的特性。

凡事有利必有弊。缺点就是由于独占锁,所以不能同时进行两个操作,这样性能上就大打折扣。从性能的角度讲LinkedBlockingDeque要比LinkedQueue要低很多,比CocurrentLinkedQueue就低更多了,这在高并发情况下就比较明显了。

前面分析足够多的Queue实现后,LinkedBlockingDeque的原理和实现就不值得一提了,无非是在独占锁下对一个链表的普通操作。

核心方法

在使用之前,让我们一起看一个简单的例子。

添加元素

移除元素

例子

我们实现 put 和 take,分别模拟写入工作者,和取出工作者。

测试代码:

日志如下:

LinkedBlockingQueue 源码

facejdk 版本

算法简介

“两个锁队列”算法的一种变体。putLock设置了看跌期权(和卖出价)的入口,并具有等待看跌期权的相关条件。

对于takeLock同样。

他们俩都依赖的“ count”字段作为原子进行维护,以避免在大多数情况下都需要获得两个锁。

同样,为了最大程度地减少获取putLock的需求,反之亦然,使用了级联通知。

当认沽权通知其至少启用了一个卖空时,它将向买受人发出信号。

如果自信号发出后输入了更多项目,则该接收者又会发信号通知其他人。

并对称地用于信令放置。

诸如remove(Object)和迭代器之类的操作均获得这两个锁。

提供作者和读者之间的可见性,如下所示:

每当将元素放入队列时,都会获取putLock并更新计数。

后续的读取器通过获取putLock(通过fullyLock)或通过获取takeLock,然后读取 来保证对排队的节点的可见性。

这样就可以看到前n个项目。

为了实现弱一致性的迭代器,看来我们需要使所有节点都可以从先前的出队节点GC到达。

这将导致两个问题:

允许恶意的Iterator导致无限的内存保留

如果某个节点在使用期间处于使用期,则导致旧节点到新节点的跨代链接,这导致了一代代GC难以处理,从而导致重复的主要集合。

但是,只有未删除的节点可以从出队节点到达,并且可达性不必一定是GC理解的那种。

我们使用链接刚刚退出队列的Node的技巧。

这样的自链接意味着前进到head.next。

类定义

实现了 BlockingQueue 接口,继承自 AbstractQueue 抽象类。

序列化

有趣的是此类支持序列化,但是Node并不支持序列化,因此fist/last就不能序列化,那么如何完成序列化/反序列化过程呢?

描述的是LinkedBlockingDeque序列化/反序列化的过程。

序列化时将真正的元素写入输出流,最后还写入了一个null。

读取的时候将所有对象列表读出来,如果读取到一个null就表示结束。

这就是为什么写入的时候写入一个null的原因,因为没有将count写入流,所以就靠null来表示结束,省一个整数空间。

内部变量

基本节点

并发控制

构造器

实际上这个默认容量这么大,感觉是用不到的。

不过既然是 LinkedList 基于链表实现,大概是想表达长度不受限制的意思。

还有一个基于集合初始化的构造器:

这里问一下大家,为什么禁止为 null 呢?

BlockingQueue 不接受 null 值的插入,相应的方法在碰到 null 的插入时会抛出 NullPointerException 异常。

null 值在这里通常用于作为特殊值返回,比如 poll() 返回 null,则代表 poll 失败。

所以,如果允许插入 null 值的话,那获取的时候,就不能很好地用 null 来判断到底是代表失败,还是获取的值就是 null 值。

enqueue 入队

这个方法写的非常的精炼。

建议看不懂的同学可以分成 2 步来看:

将 node 节点放在队列的最后,将 last.next 变为新的队尾。

put 放入元素

我们来重点看一下 put() 方法。

signalNotEmpty

这个方法是通知 notEmpty,便于元素可以取出。

当然这里的对于 notEmpty 的唤醒,是通过 takeLock 保证并发安全的。

这样看来,LinkedBlockingQuue 和 ArrayBlockingQueue 实现上还是有很大差异的。

take 获取元素

获取元素的实现如下:

dequeue 出队方法

signalNotFull 唤醒 notFull

小结

本文从 LinkedBlockingQueue 的入门使用,逐渐深入到源码分析。

实际上原理都是类似的,不过这个对比 ArrayBlockingQueeu 的锁粒度更加细致。

没读源码之前,我一直以为二者只是链表和数组的区别。

很多事情,都不是我们以为的那个样子。

希望本文对你有帮助,如果有其他想法的话,也可以评论区和大家分享哦。

各位极客的点赞收藏转发,是老马持续写作的最大动力!

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20201107A06AEN00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券
http://www.vxiaotou.com