我们之前写过关于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框架提供的各种线程池,同样无法离开队列。
对于队列本身的考点,主要有以下几点:
-
哪些队列是有界的,哪些队列是无界的
-
如果在不同的场景选择合适的队列实现
-
线程队列安全是如何实现的,并且做了哪些提升性能的做法
扩展
哪些队列是有界的,哪些队列是无界的
常见的集合中如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实现比较简单,性能更好预测,属于表现稳定的“选手”。