问题
输入图片说明
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 的锁粒度更加细致。
没读源码之前,我一直以为二者只是链表和数组的区别。
很多事情,都不是我们以为的那个样子。
希望本文对你有帮助,如果有其他想法的话,也可以评论区和大家分享哦。
各位极客的点赞收藏转发,是老马持续写作的最大动力!
领取专属 10元无门槛券
私享最新 技术干货