简介
在并发编程中,有时候需要使用线程安全的队列。
要实现一个线程安全的队列有两种方式:
1. 阻塞算法;
阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现。
2. 非阻塞算法。
非阻塞的实现方式则可以使用循环CAS的方式来实现。
JUC中非阻塞队列有ConcurrentLinkedQueue和ConcurrentLinkedDeque。本文主要介绍阻塞队列相关类和接口
阻塞队列
阻塞队列在实际应用中非常广泛,许多消息中间件中定义的队列,通常就是一种“阻塞队列”。
ConcurrentLinkedQueue
和ConcurrentLinkedDeque是以非阻塞算法实现的高性能队列,其使用场景一般在高并发环境下,需要“队列/栈”这类数据结构时才使用;
而 “阻塞队列” 通常利用了“锁”来实现,也就是会阻塞调用线程,其使用场景一般是在 “生产者-消费者” 模式中,用于线程之间的数据交换或系统解耦。
“生产者-消费者”这种模式中,“生产者” 和 “消费者” 是相互独立的,两者之间的通信需要依靠一个队列。这个队列就是要说的阻塞队列。
引入“阻塞队列”的最大好处就是解耦,在软件工程中,“高内聚,低耦合”是进行模块设计的准则之一,这样“生产者”和“消费者”其实是互不影响的,将来任意一方需要升级时,可以保证系统的平滑过渡。
什么是阻塞队列
阻塞队列BlockingQueue是一个支持两个附加操作的队列。提供了一些阻塞方法,主要作用是:
1) 支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满。
2) 支持阻塞的移除方法:在队列为空时,获取元素的线程会等待队列变为非空。
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列是生存者用来存放元素,消费者用来获取元素的容器。
java里的阻塞队列
JDK提供了7种阻塞队列。如下:
1)ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
2)LinkedBlockingQueue:由链表结构组成的有界阻塞队列。
3)PriorityBlockingQueue:支持优先级排序的无界阻塞队列
4)DelayQueue:使用优先级队列实现的无界阻塞队列
5)SynchronousQueue:不存储元素的阻塞队列
6)LinkedTransferQueue:由链表结构组成的无界阻塞队列
7)LinkedBlockingQueue:由链表结构组成的双向阻塞队列
7种阻塞队列介绍
ArrayBlockingQueue
ArrayBlockingQueue是一个用数组实现的有界阻塞队列,在初始化构造的时候需要指定队列的容量。此队列按照先进先出(FIFO)的原则对元素进行排序。
具有如下特点:
1. 队列的容量一旦在构造时指定,后续不能改变;
2. 插入元素时,在队尾进行;删除元素时,在队首进行;
3. 队列满时,调用特定方法插入元素会阻塞线程;队列空时,删除元素也会阻塞队列;
4. 支持公平/非公平策略,默认为非公平策略。
这里的公平策略,是指当线程从阻塞到唤醒后,以最初请求的顺序(FIFO)来添加或删除元素;非公平策略指线程被唤醒后,谁先抢占到锁,谁就能往队列中添加/删除,顺序是随机的。
ArrayBlockingQueue利用了ReentrantLock来保证线程的安全性,针对队列的修改都需要加全局锁。
在一般的应用场景下已经足够。对于超高并发的环境,由于生产者-消息者共用一把锁,可能出现性能瓶颈。
LinkedBlockingQueue
LinkedBlockingQueue是一个用链表实现的有界阻塞队列。此队列的默认和最大长度是Integer.MAX_VALUE。此队列按照先进先出(FIFO)的原则对元素进行排序。
LinkedBlockingQueue除了底层数据结构是单链表与ArrayBlockingQueue不同外,另外一个特点就是:它维护了两把锁——takeLock
和putLock。
takeLock用于控制出队的并发,putLock用于控制入队的并发。
这也就意味着,同一时刻,只能只有一个线程能执行入队/出队操作,其余入队/出队线程会被阻塞; 但是,入队和出队之间可以并发执行,即同一时刻,可以同时有一个线程进行入队,另一个线程进行出队,这样就可以提升吞吐量。
LinkedBlockingQueue和ArrayBlockingQueue比较主要有以下区别:
- 队列大小不同。ArrayBlockingQueue初始构造时必须指定大小,而LinkedBlockingQueue构造时既可以指定大小,也可以不指定(默认为
Integer.MAX_VALUE
,近似于无界);- 底层数据结构不同。ArrayBlockingQueue底层采用数组作为数据存储容器,而LinkedBlockingQueue底层采用单链表作为数据存储容器;
- 两者的加锁机制不同。ArrayBlockingQueue使用一把全局锁,即入队和出队使用同一个ReentrantLock锁;而LinkedBlockingQueue进行了锁分离,入队使用一个ReentrantLock锁(putLock),出队使用另一个ReentrantLock锁(takeLock);
- LinkedBlockingQueue不能指定公平/非公平策略(默认都是非公平),而ArrayBlockingQueue可以指定策略。
PriorityBlockingQueue
PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序升序排列。在构造的时候可以指定队列的初始容量。
1. PriorityBlockingQueue是一种优先级队列,也就是元素并不是以FIFO的方式出/入队,而是按照权重大小的顺序出队;
2. PriorityBlockingQueue是真正的无界队列(仅受内存大小限制),它不像ArrayBlockingQueue那样构造时必须指定最大容量,也不像LinkedBlockingQueue默认最大容量为Integer.MAX_VALUE;
3. 由于PriorityBlockingQueue是按照元素的权重进入排序,所以队列中的元素必须是可以比较的,也就是说元素必须实现Comparable接口;
4. 由于PriorityBlockingQueue无界队列,所以插入元素永远不会阻塞线程;
5. PriorityBlockingQueue底层是一种基于数组实现的堆结构。
注意:堆分为“大顶堆”和“小顶堆”,PriorityBlockingQueue会依据元素的比较方式选择构建大顶堆或小顶堆。比如:如果元素是Integer这种引用类型,那么默认就是“小顶堆”,也就是每次出队都会是当前队列最小的元素。
PriorityBlockingQueue属于比较特殊的阻塞队列,适用于有元素优先级要求的场景。它的内部和ArrayBlockingQueue一样,使用一个了全局独占锁来控制同时只有一个线程可以进行入队和出队,另外由于该队列是无界队列,所以入队线程并不会阻塞。
PriorityBlockingQueue始终保证出队的元素是优先级最高的元素,并且可以定制优先级的规则,内部通过使用堆(数组形式)来维护元素顺序,它的内部数组是可扩容的,扩容和出/入队可以并发进行。
SynchronousQueue
SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。
它支持公平访问队列。默认情况下线程采用非公平性策略访问队列。通过构造入参创建公平性访问的SynchronousQueue,如果设为true,则等待的线程会采用先进先出的顺序访问队列。
SynchronousQueue的底层实现包含两种数据结构——栈和队列。这是一种非常特殊的阻塞队列。
特点简要概述如下:
1. 入队线程和出队线程必须一一匹配,否则任意先到达的线程会阻塞。比如ThreadA进行入队操作,在有其它线程执行出队操作之前,ThreadA会一直等待,反之亦然;
2. SynchronousQueue内部不保存任何元素,也就是说它的容量为0,数据直接在配对的生产者和消费者线程之间传递,不会将数据缓存到队列中。
3. SynchronousQueue支持公平/非公平策略。其中非公平模式,基于内部数据结构——“栈”来实现,公平模式,基于内部数据结构——“队列”来实现;
DelayQueue
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
DelayQueue非常有用,可以将DelayQueue运用在以下应用场景
1) 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
2) 定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间。
它的特点:
1. DelayQueue是无界阻塞队列;
2. 队列中的元素必须实现Delayed接口,元素过期后才会从队列中取走;
LinkedBlockingDeque
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。
和ConcurrentLinkedDeque类似,都是一种双端队列的结构,只不过LinkedBlockingDeque同时也是一种阻塞队列。
LinkedBlockingDeque底层利用ReentrantLock实现同步,并不像ConcurrentLinkedDeque那样采用无锁算法。
LinkedBlockingDeque作为一种阻塞双端队列,提供了队尾删除元素和队首插入元素的阻塞方法。
该类在构造时一般需要指定容量,如果不指定,则最大容量为
Integer.MAX_VALUE
。另外,由于内部通过ReentrantLock来保证线程安全,所以LinkedBlockingDeque的整体实现时比较简单的。另外,双端队列相比普通队列,主要是多了【队尾出队元素】/【队首入队元素】的功能。
阻塞队列我们知道一般用于“生产者-消费者”模式,而双端阻塞队列在“生产者-消费者”就可以利用“双端”的特性,从队尾出队元素。
LinkedTransferQueue
LinkedTransferQueue一种比较特殊的阻塞队列。
我们知道,在普通阻塞队列中,当队列为空时,消费者线程(调用take或poll方法的线程)一般会阻塞等待生产者线程往队列中存入元素。而LinkedTransferQueue的transfer方法则比较特殊:
- 当有消费者线程阻塞等待时,调用transfer方法的生产者线程不会将元素存入队列,而是直接将元素传递给消费者;
- 如果调用transfer方法的生产者线程发现没有正在等待的消费者线程,则会将元素入队,然后会阻塞等待,直到有一个消费者线程来获取该元素。
LinkedTransferQueue的特点简要概括如下:
- LinkedTransferQueue是一种无界阻塞队列,底层基于单链表实现;
- LinkedTransferQueue中的结点有两种类型:数据结点、请求结点;
- LinkedTransferQueue基于无锁算法实现。
LinkedTransferQueue兼具了SynchronousQueue的特性以及无锁算法的性能,并且是一种无界队列:
和SynchronousQueue相比,LinkedTransferQueue可以存储实际的数据;
和其它阻塞队列相比,LinkedTransferQueue直接用无锁算法实现,性能有所提升。
LinkedTransferQueue
包含了ConcurrentLinkedQueue、SynchronousQueue、LinkedBlockingQueues
三种队列的功能
总结
队列特性 | 有界队列 | 近似无界队列 | 无界队列 | 特殊队列 |
---|---|---|---|---|
有锁算法 | ArrayBlockingQueue | LinkedBlockingQueue、LinkedBlockingDeque | / | PriorityBlockingQueue、DelayQueue |
无锁算法 | / | / | LinkedTransferQueue | SynchronousQueue |