Java中的阻塞队列

Java中的阻塞队列

ArrayBlockingQueeue,LinkedBlockingQueue,PriorityBlockingQueue,ConcurrentLinkedQueue,SynchronousQueue,DelayQueue 简单对比

ArrayBlockingQueue

特性

  • 数组存储
  • 有界
  • 阻塞队列
  • FIFO

初始化参数

  • capacity 队列最大可用容量
  • fair 阻塞用的锁是否使用公平锁, 默认为false
  • collection 队列初始化完成后,将collection中的元素放入队列中

基本原理

  1. 使用ReentrantLock保证同一时刻只有一个线程对队列进行读或者写
  2. 使用两个Condition分别来控制生产者和消费者是否进行阻塞,以及是否取消阻塞
  3. 容量在创建时即初始化好,并分配空间,不可改变
  4. 内部维护三个变量takeIndex(尾索引),putIndex(头索引), count(实际大小),因为只有一把锁,所以count不需要特殊修饰符修饰

LinkedBlockingQueue

特性

  • 链表存储
  • 可设置边界
  • 阻塞队列
  • FIFO

初始化参数

  • capacity 队列的最大容量,默认为:Integer.MAX_VALUE
  • collection 队列初始化完成后,将collection中的元素放入队列中

基本原理

  1. 使用ReentrantLock分别创建了两把锁,读锁和写锁
  2. 通过读锁和写锁各自的信号量单独控制生产者和消费者是否阻塞
  3. 只设置最大容量,不预先分配空间
  4. 内部维护head(头指针),last(尾指针),以及AtomicInteger类型的count(实际大小),因为是两把锁,所以count需要在能够在不同线程间通讯。

LinkedBlockingQueue与ArrayBlockingQueue对比

这两个队列的区别主要是因为存储数据的结构不同引起的.

1. array和LinkedList数据结构不同导致Queue处理capacity参数的策略不同

  • array初始化时,必须设置大小,并且初始化,所以ArrayBlockingQueue必须初始化容量,并且当容量设置过大时, 在初始化时,就有内存溢出的风险
  • 而链表初始化时,仅仅初始化了一个头尾指针,所以LinkedBlockingQueue的容量大小可以设置为Integer.MAX_VALUE。

2. array和LinkedList数据结构不同导致Queue内部设计锁的策略不同

从array和linkedList都是可以分别设置读写锁的,那为什么ArrayBlockingQueue公用一把锁呢?

  1. ArrayBlockingQueue设置读锁和写锁,就需要考虑count就需要使用AtomicInteger,而array在操作元素的时候,只需要更新数组的指针指向,读写速度相对一致,此时AtomicInter类型的count有可能成为性能瓶颈(乐观锁遇见同时修改数据的情况,性能较低),所以不如直接使用一把锁,性能基本相似,并且代码复杂度降低
  2. 当Queue的容量为空的时候,此时读锁和写锁需要控制array的同一个位置,此时的性能和使用一把锁是一样的。而且从实际场景来看,我们无法判断这种情形的多少,倒不如减少代码复杂度,
  3. linkedList因为是使用的链表,即使队列为空时,其也是分别操作头尾指针,所以采用读锁和写锁可以降低抢占锁的性能
  4. linkedList 插入数据时,需要先构建node,而取数据时,只需要分离指针即可,读写操作耗时不一致,相当于降低了同时修改cout值的概率,此时乐观锁效率较高

3. 为什么通常说 LinkedBlockingQueue比ArrayBlockingQueue速度快,但是LinkedBlockingQueue的性能却是不可预测或者说不稳定的呢?

快是因为双锁比单锁要快,不稳定是因为乐观锁,乐观锁在并发过高的时候性能不稳定。

PriorityBlockingQueue

特性

  • 数组存储
  • 无界
  • 阻塞队列
  • 根据执行策略进行优先级排序出列

初始化参数

  • initialCapacity 初始容量,默认为11,设定初始容量可以在能够预估容量的情况下,避免因为多次扩容造成的性能浪费
  • comparator 排序规则,默认采用自然排序
  • collection 将collection中的元素放入队列中

基本原理

  1. 使用二叉树最小堆算法来维护内部数组
  2. 使用ReentrantLock保证同一时刻只有一个线程对队列进行读或者写
  3. 使用一个Condition控制消费者是否进行阻塞,以及是否取消阻塞
  4. 容量可动态扩容
  5. 插入数据时,根据预设排序规则,将元素插入指定位置,
  6. 取数据时,从队列头部取,因为头部的数据优先级总是最高的

QA

  1. Q: 为什么不用没有使用链表实现的优先级队列?
    A: 因为优先级队列涉及集合的排序过程,而大多数排序算法都需要随机插入,在随机插入方面数组的效率是远优于链表的。所以使用数组而不是链表。
  2. Q: PriorityBlockingQueue最大容量为什么是Integer.MAX_VALUE - 8?
    A: 作者的解释是:有些jvm会在数组放一下头信息,为了避免内存溢出,所以设置最大容量为Integer.MAX_VALUE - 8, 在Java中,几乎所有以数组作为存储结构的工具类都是这么设计的。比如ArrayList。那为什么要减8呢?因为jvm的header信息在32位的系统下是4字节,在64位系统下是8字节。所以预留减8足够了。

ConcurrentLinkedQueue

特性

  • 链表存储
  • 无界
  • 并发安全
  • FIFO

基本原理

ConcurrentLinkedQueue是基于乐观锁(CAS)实现的并发安全的无界队列。其使用乐观锁存取数据,避免了在等待锁时,因线程挂起导致上下文切换所耗费的性能。
在引入乐观锁的同时, ConcurrentLinkedQueue引入惰性更新的逻辑,延迟更新tail节点的更新频率。即,正常操作应该是每次存取数据都要更新tail节点,但是ConcurrentLinkedQueue只有在tail的操作次数达到阈值后才会被更新,这样就减少了锁的次数。降低性能开销。

LinkedTransferQueue

特性

  • 链表存储
  • 无界
  • 并发安全
  • FIFO
  • 阻塞

基本原理

LinkedTransferQueue扩展了内部存储的数据类型,即 懒惰双类型队列(Dual Queues with Slack),一般的队列,内部只是缓存生产者产生的数据,这种情况下,从生产者到消费者,数据流转必须经历一个完整的入队,出队过程,即使此时消费者的能力是大于等于生产者的。
而LinkedTransferQueue不仅可以缓存生产者产生的数据,还可以缓存消费者线程。通过这种方式,将数据交换变为:插入数据时,判断有没有消费者在等待,若有则直接将数据交给消费者,没有这加入队列缓存。而消费者在取数据时,若发现队列为空,此时是将自己缓存到队列的尾节点中。通过这种设计,一方面减少了消费者能力大于等于生产者条件下的数据交换逻辑,同时减少了锁的开销。
LinkedTransferQueue在更新tail节点时,也引入惰性删除的逻辑。

SynchronousQueue

特性

  • 阻塞队列
  • 零容量
  • 阻塞
  • FIFO

初始化参数

  • fail 是否使用公平锁,默认为false

基本原理

SynchronousQueue分公平模式和非公平模式。

  • 公平模式内部实现了一个TransferQueue,该队列的特点是不存储数据,然后将提交数据的生产者和消费数据的消费者阻塞起来,当生产者线程和消费者线程同时出现时,即可直接交换数据。 TransferQueue内部实现了一个链表,用于存储生产者和消费者线程。当没有成对的生产者消费者出现时,所有来存取数据的线程都会被阻塞在链表中。直到有匹配线程出现,则按照入队顺序唤醒相应的线程,然后交换数据。

  • 非公平模式内部实现了一个TransferStack, 即使用栈的先进后出特性实现非公平性。

DelayQueue

特性

  • 延时
  • 阻塞
  • 无界

基本原理

DelayQueue内部使用PriorityQueue实现了一个针对数据缓存时间的优先级队列。所以其特性同PriorityBlockingQueue是相同的。

你可能感兴趣的:(Java中的阻塞队列)