java并发面试常识之LinkedBlockingQueue
谈到ArrayBlockingQueue的特色就是循环队列,然后一把锁,2个条件,完成了功能。本来以为LinkedBlockingQueue也是这样的,结果和预期不一样,LinkedBlockingQueue利用了链表的特点,使用了两把锁,两个条件来控制。是一个锁分离的应用,下面就说说,他的实现,以及为什么ArrayBlockingQueue就不适合锁分离。
主要成员变量
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();
private final AtomicInteger count = new AtomicInteger();
除了两个锁,两个条件外,我这里专门列举了计数器。这个计数器很重要,重要到锁分离要依赖他才能正常运行。
锁分离
使用双锁分离就得注意一点,那就是防止线程夯死。生产线程要唤醒生产线程,消费线程也要唤醒生产线程,消费线程唤醒消费线程,消费线程也要唤醒生产线程。
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
//唤醒标记
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
//阻塞生产线程
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
//唤醒生产线程
notFull.signal();
} finally {
putLock.unlock();
}
//唤醒消费线程
if (c == 0)
signalNotEmpty();
}
生产线程唤醒消费线程
基于上面的介绍,我们来看代码,唤醒标记就是为了生产唤醒消费的,因为可能出现消费线程全部都已经等待了,此时生产线程运作,但是消费线程并不能自己唤醒自己,于是就有了signalNotEmpty()的操作。这里的c是getAndIncrement的值,就是获取计数之前的值。c==0的满足条件就有1个元素,在这种情况下才去唤醒消费线程。
生产线程唤醒生产线程
在获取锁后,如果发现容量达到上限,就阻塞了,等待被唤醒,如果可以加入,就执行enqueue方法,是个很简单的链表添加节点的方法。就是在原来last节点后加节点,然后更新last节点。
private void enqueue(Node<E> node) {
last = last.next = node;
}
在计数器自增后,判断唤醒标记,如果还能继续生产,就去唤醒生产线程。
消费的方案思想和生产类似,这里就不说代码了。
删除
public boolean remove(Object o) {
if (o == null) return false;
fullyLock();
try {
for (Node<E> trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
if (o.equals(p.item)) {
unlink(p, trail);
return true;
}
}
return false;
} finally {
fullyUnlock();
}
}
删除代码相对比较简单,主要是要获取两把锁,才能进行删除操作就是fullyLock()和fullyUnlock(),删除掉元素后,还要唤醒生产线程。
void unlink(Node<E> p, Node<E> trail) {
p.item = null;
trail.next = p.next;
if (last == p)
last = trail;
if (count.getAndDecrement() == capacity)
notFull.signal();
}
ArrayBlockingQueue为何不适合锁分离
这个主要是循环队列的原因,主要是数组和链表不同,链表队列的添加和头部的删除,都是只和一个节点相关,添加只往后加就可以,删除只从头部去掉就好。为了防止head和tail相互影响出现问题,这里就需要原子性的计数器,头部要移除,首先得看计数器是否大于0,每个添加操作,都是先加入队列,然后计数器加1,这样保证了,队列在移除的时候,长度是大于等于计数器的,通过原子性的计数器,双锁才能互不干扰。数组的一个问题就是位置的选择没有办法原子化,因为位置会循环,走到最后一个位置后就返回到第一个位置,这样的操作无法原子化,所以只能是加锁来解决。
适用场景
LinkedBlockingQueue的优点是锁分离,那就很适合生产和消费频率差不多的场景,这样生产和消费互不干涉的执行,能达到不错的效率,尽量不使用remove操作,获取两把锁的效率更低,可以使用size方法(就是计数器直接返回),这个还是比较重要的,有些集合不适合使用size,例如ConcurrentLinkedQueue,正确应该使用isEmpty()。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
我们为什么以及是如何从Angular.js 迁移到 Vue.js?
一些背景介绍 我们的应用程序( Holistics.io )是一个基于 SQL的商业智能(BI)平台,使用Rails、Sidekiq、PostgreSQL和AngularJS编写。我们的Rails应用程序始于2013年底,作为一个简单的应用程序其中使用了jQuery和AngularJS。我们使用AngularJS主要特性/功能如下: · 查看模型绑定(控制器,视图 +模板引擎) · 依赖注入(服务,工厂,指令) · Angular第三方组件(uib-modal,ui-select,…) 其余的都是内部的自定义 JavaScript。 我们在 Angular 中遇到的问题 随着我们应用的升级,我们在使用 AngularJS 的时候遇到了这样一些问题: · 渲染性能: 作为数据工具,由于 AngularJs的特性,我们不得不花大量的时间来呈现一张巨大的数据表。 · Angular 的文档不太好: 在这成为问题之前,其他都不算什么问题。我们越深入地使用 AngularJS,就越觉得它的文档实在难以理解。 · 双向数据流 使得逻辑处理起来相当困难,不管是写组件还是写视图控制器都是如此。这可能是...
-
下一篇
2018年6月中旬值得一读的8本后端开发技术书籍!
RocketMQ实战与原理解析杨开元 著 阿里巴巴数据专家/RocketMQ源码贡献者撰写,RocketMQ官方开发团队鼎力推荐!从开发和运维双重视角,详细讲解如何使用和用好RocketMQ,以及如何基于RocketMQ源码进行定制和二次开发。 Kafka源码解析与实战王亮 著 资深架构师多年工作经验总结,2.包含Kafka源代码分析与内部的实现原理,以及外部的维护工具、客户端编程、与第三方集成方式,本书穿插了大量的图片,讲解细致、生动有趣。 Akka实战:快速构建高可用分布式应用杜云飞 著 资深Java技术专家在大量实践后的经验与教训的总结。以实战为导向,从Akka架构、组件、工具包、分布式、集群、微服务等多个角度展开,为快速构建高性能、高可用的应用提供良好解决方案。为快速掌握Akka技术细节和实践方法提供系统、详尽指导。 深入分布式缓存于君泽,曹洪伟,邱硕 等著 来自蚂蚁金服、京东、网联、新浪微博、同程旅游等公司的10余位一线架构师用心之作。深度解构Ehcache、Memcached、Redis、tair、EVCache、Aerospike等6大缓存系统的技术原理及其在电商、社交、...
相关文章
文章评论
共有0条评论来说两句吧...