并发包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别?

我们之前写过关于concurrent包下的hashmap,分析过其原理性能以及和非这个包下的hashTable做过对比,这次就来介绍一下concurrent包下的两个类,ConcurrentLinkedQueue和LinkedBlockingQueue,两个线程安全队列的使用。

概述

有时候我们把并发包下的所有容器都习惯的称为并发容器,但是严格来讲,只有Concurrent*才是真的并发容器。

关于问题中的区别在于:

  • Concurrent类型基于lock-free,在常见的多线程访问场景,一般可以提供较高吞吐量。

lock-free:如果在一个共享的数据结构上的操作都不需要互斥,那么它是无锁的。如果一个进程在操作中间被中断,其它进程不受影响。
举例:如果一个线程拥有互斥锁,当运行线程的时候处于休眠,就会block,但lock-free模式下不会阻塞。

  • LinkedBlockingQueue内部是基于锁,提供BlockingQueue等特性方法

同样是线程安全的容器,可简单认为:

  • Concurrent类型没有类似CopyOnWrite之类的容器相对较重的修改开销

  • Concurrent拥有较低的遍历一致性,也就是弱一致性。当使用遍历方法中的迭代器遍历时,容器发生修改,迭代器仍然可以继续遍历
    与弱一致性相对的,如果遍历过程出了问题,就会抛出异常ConcurrentModifcationException,停止遍历。

  • concurrent的弱一致性,导致对size的操作精准度有限。

  • concurrent读取性能不佳。

问题思考

队列是非常重要的数据结构,在日常开发中,很多线程间的数据传输都需要依赖于它,Executor框架提供的各种线程池,同样无法离开队列。

对于队列本身的考点,主要有以下几点:

  1. 哪些队列是有界的,哪些队列是无界的

  2. 如果在不同的场景选择合适的队列实现

  3. 线程队列安全是如何实现的,并且做了哪些提升性能的做法

扩展

哪些队列是有界的,哪些队列是无界的

常见的集合中如LinkedList 是个 Deque,只不过不是线程安全的。可以看下图的线程队列实现:

在这里插入图片描述

从数据结构的角度去看,实现了Deque的两个具体类,ConcurrentLinkedDeque和LinkedBlockingDeque都拥有Deque的侧重点,那就是同时支持头插法和尾插法

  • 尾部插入时需要的addLast(e)、oferLast(e)。
  • 尾部删除所需要的removeLast()、pollLast()。

从行为特征来看,绝大部分Queue都是实现了BlockingQueue接口。在常规队列操作基础上,Blocking意味着其提供了特定的等待性操作,获取时(take)等待元素进队,或者插 入时(put)等待队列出现空位。

另一个BlockingQueue经常被考察的点,就是是否有界(Bounded、Unbounded),总结如下:

  • ArrayBlockingQueue是最典型的的有界队列,其内部以fnal的数组保存数据,数组的大小就决定了队列的边界,所以创建要指定容量
public ArrayBlockingQueue(int capcity,boolean fair)
  • LinkBlockingQueue,有界,但是如果在创建的时候不指明capcity,就会默认为MAX_VALUE,也就是变相的无界

  • SynchronousQueue,每一个删除操作都要等待插入,反之亦然,所以其内部容量为0。

  • PriorityBlockingQueue是无边界的优先队列,虽然严格意义上来讲,其大小总归是要受系统资源影响

  • DelayedQueue和LinkedTransferQueue同样是无边界的队列。

对于无边界的队列,有一个自然的结果,就是put操作永远也不会发生其他BlockingQueue的那种等待情况。

如果看底层的话BlockingQueue也基本都是基于锁去实现的,我们可以看看典型的LinkedBlockingQueue

    /** Lock held by take, poll, etc */
    private final ReentrantLock takeLock = new ReentrantLock();

    /** Wait queue for waiting takes */
    private final Condition notEmpty = takeLock.newCondition();

    /** Lock held by put, offer, etc */
    private final ReentrantLock putLock = new ReentrantLock();

    /** Wait queue for waiting puts */
    private final Condition notFull = putLock.newCondition();

然后在和我们在之前将ReentrantLock专栏提到的ArrayBlockingQueue对比一下

public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

我们可以看出,notEmpty和notFull在ArrayBlockingQueue中是同一个再入锁实现的变量,而在LinkedBlockingQueue使用了不同的再入锁去实现,所以具备更高的吞吐量。

类似ConcurrentLinkedQueue等,则是基于CAS的无锁技术,不需要在每个操作时使用锁,所以扩展性表现要更加优异。

相对比较另类的SynchronousQueue,在Java 6中,其实现发生了非常大的变化,利用CAS替换掉了原本基于锁的逻辑,同步开销比较小。它 是Executors.newCachedThreadPool()的默认队列。

队列的使用场景和举例

队列广泛用在生产-消费模型中,利用BlockingQueue的方法实现可以减少很多协调工作。可参考一下代码:

生产者:
在这里插入图片描述
消费者:
在这里插入图片描述

启动类:
在这里插入图片描述

输出:
在这里插入图片描述

我们可以看出,使用Blocking的队列更加方便,不然就得自己判断轮询,空值。

如何选择队列应用到开发

以上面举出的LinkedBlockingQueue、ArrayBlockingQueue为例子,需求可以从很多方面去考虑:

  • 考虑应用场景中对队列边界的要求。ArrayBlockingQueue是有明确的容量限制的,而LinkedBlockingQueue则取决于我们是否在创建时指定,SynchronousQueue则干脆不 能缓存任何元素。

  • 从空间利用角度,数组结构的ArrayBlockingQueue要比LinkedBlockingQueue紧凑,因为其不需要创建所谓节点,但是其初始分配阶段就需要一段连续的空间,所以初始内存 需求更大。

  • 通用场景中,LinkedBlockingQueue的吞吐量一般优于ArrayBlockingQueue,因为它实现了更加细粒度的锁操作。

  • ArrayBlockingQueue实现比较简单,性能更好预测,属于表现稳定的“选手”。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值