Java-并发-队列-阻塞和非阻塞队列总结
转载声明:
本文系转载自以下文章:
- Java 并发 — 阻塞队列总结
作者: thgreat - java中并发集合
作者:hy飞无
0x01 摘要
本文会对java并发包内的常用重要阻塞/非阻塞队列进行总结。
0x02 非阻塞式集合
这类集合也包括添加和移除的方法,如果方法不能立即被执行,则返回null或抛出异常,但是调用这个方法的线程不会被阻塞。
2.1 ConcurrentLinkedQueue
基于链接节点的无限制线程安全队列,此队列命令元素FIFO(先进先出)。这个队列在add(),remove(),poll()都用了cas来保证安全。
在iterator()时,如果集合被改变,那么数据可能会不一致。
2.2 ConcurrentLinkedDeque
- 基于链接节点的无界并发的双端队列。
- 并发插入,删除和访问操作安全执行。
- 因为这些deques的异步性质,确定当前的数量,元素需要遍历元素,因此可以报告,如果在遍历期间修改此集合,则结果不准确。
- 也是使用cas来保证线程安全,这个类不仅可以操控头部,也可以操控尾部。
0x03 阻塞式集合
这类集合包括添加和移除的数据方法。当集合已满或为空时,被调用的添加或者移除方法就不能立即被执行,那么调用这个方法的线程将被阻塞,一直到该方法可以被成功执行。
3.2 ArrayBlockingQueue
ArrayBlockingQueue 特点:
- 用数组实现
- 有界阻塞队列
初始化时需要指定队列的大小,它不会像ArrayList那样自动扩容。 - FIFO
- ArrayBlockingQueue 是一种逻辑上的环形队列。
- 线程安全
Vector
用synchronized
关键字进行线程同步,而ArrayBlockingQueue
中通过ReentrantLock来完成的。 - 入队和出队不可并发
ArrayBlockingQueue 在入队和出队上都使用了同一个重入锁,因此入队和出队是不能并发执行的。 - 可重入锁和Condition:
- 可重入锁是独占式的锁,来保证对队列的访问都是线程安全的阻塞操作。ReentrantLock 有公平锁和非公平锁之分,因此需要指定,默认是非公平锁。
- Condition控制什么情况下该阻塞或不阻塞。
源码分析请参考:Java 并发 — 阻塞队列之ArrayBlockingQueue源码分析
3.3 LinkedBlockingQueue
LinkedBlockingQueue特点如下:
- 用链表实现
- 有界阻塞队列
- FIFO
- LinkedBlockingQueue 和ArrayBlockingQueue 是差不多的,只是储存方式由数组转换为链表了而已,LinkedBlockingQueue 也是通过重入锁和Condition
来对队列的操作访问进行控制。 - 入队和出队可并发
在ArrayBlockingQueue中,入队和出队都是用的同一个可重入锁,出队入队不可并发;而LinkedBlockingQueue 对于出队和入队使用了不同的可重入锁来控制 - 对于队列中元素的个数使用了原子类AtomicInteger,来保证对count的操作具有原子性。
自己实现LinkedBlockingQueue:Java-并发-自己实现阻塞队列
源码分析请参考:Java 并发 — 阻塞队列之PriorityBlockingQueuey源码分析
3.4 PriorityBlockingQueue
先看看继承关系:
PriorityBlockingQueue 特点:
- 支持优先级
- 无界阻塞队列
PriorityBlockingQueue 是无界队列,不会“队满”。实际当到达队列最大值后(Integer.MAX_VALUE - 8,减8的原因是:数组作为一个对象,需要一定的内存存储对象头信息,对象头信息最大占用内存不可超过8字节。),就抛OOM异常了,因此这点在使用优先队列的时候需要注意。 - 放入队列的元素必须实现
Comparable
接口的public int compareTo(T o)
方法 - 默认情况下
comparator
为null
。此时会根据元素的compareTo
方法来来排序,比如String
类型就是按其实现的挨个比较内部char
数组的Ascii
码来排序。采取从小到大顺序升序排列,也可以自定义类实现compareTo()方法来指定元素排序规则,需要注意的是不能保证同优先级元素的顺序。 - 线程安全
通过一个可重入锁ReentrantLock来控制入队和出队操作 - 入队出队不可并发
- 通过二叉堆来实现
默认情况下,父元素的值永远比任何子元素的值还小。也就是说,最小的值是queue[0]
二叉堆 使用的是数组来实现,对一个二叉树进行编号,然后按照顺序存放在数组中。
观察一下父子节点之间的编号,会发现如果节点在数组中的位置是i(i是节点在数组中的下标), 则i节点对应的子节点在数组中的位置分别是 2i + 1
和 2i + 2
,同时i
的父节点的位置为 (i-1)/2
。
因此现在我们就把树存储到了数组中,同时通过这种规律可以很快找到每个节点的父亲和孩子节点。
关于二叉堆的更多操作,可以看上面的链接,这里就不赘述了,简单提一下就好。
源码分析请参考:Java-并发-队列-PriorityBlockingQueue
3.5 DelayQueue
- DelayQueue 内部通过组合PriorityQueue 来实现存储和维护元素顺序的,其存储元素必须实现Delayed 接口。通过实现Delayed 接口,可以获取到元素延迟时间,以及可以比较元素大小(Delayed 继承Comparable)
- DelayQueue 通过一个可重入锁来控制元素的入队出队行为
- DelayQueue 中leader 标识 用于减少线程的竞争,表示当前有其它线程正在获取队头元素。
- PriorityQueue 只是负责存储数据以及维护元素的顺序,对于延迟时间取数据则是在DelayQueue 中进行判断控制的。
源码分析请参考:Java 并发 — 阻塞队列之DelayQueue源码分析
3.6 SynchronousQueue
-
SynchronousQueue 不是一个真正的队列,其主要功能不是存储元素,而且维护一个排队的线程清单。这些线程等待把元素加入或者移出队列。每一个线程的入队(出队)操作必须等待另一个线程的出队(入队)操作。
-
SynchronousQueue中的队列不是针对数据的,而是针对操作,也就是入队不一定就是入队数据,而是入队的操作,操作可以是put,也可以是take,put操作与take操作对应,可以互相匹配,put和put,take和take则是相同的操作(模式)。
-
SynchronousQueue 并没有使用锁来保证线程的安全,使用的是循环CAS方法。
SynchronousQueue有两种模式:- 公平模式
所谓公平就是遵循先来先服务的原则,因此其内部使用了一个FIFO队列来实现其功能。 - 非公平模式
SynchronousQueue 中的非公平模式是默认的模式,其内部使用栈来实现其功能,也就是后来的先服务,
- 公平模式
源码分析请参考:Java 并发 — 阻塞队列之SynchronousQueue源码分析
3.7 LinkedTransferQueue
- LinkedTransferQueue 和SynchronousQueue 其实基本是差不多的,两者都是无锁带阻塞功能的队列
- SynchronousQueue 通过内部类Transferer 来实现公平和非公平队列
- LinkedTransferQueue 中没有公平与非公平的区分
- LinkedTransferQueue 实现了TransferQueue接口,该接口定义的是带阻塞操作的操作,相比SynchronousQueue 中的Transferer 功能更丰富。
- LinkedTransferQueue是基于链表的FIFO无界阻塞队列,它是JDK1.7才添加的阻塞队列,有4种操作模式:
private static final int NOW = 0; // for untimed poll, tryTransfer
private static final int ASYNC = 1; // for offer, put, add
private static final int SYNC = 2; // for transfer, take
private static final int TIMED = 3; // for timed poll, tryTransfer
- NOW :如果在取数据的时候,如果没有数据,则直接返回,无需阻塞等待。
- ASYNC:入队的操作都不会阻塞,也就是说,入队后线程会立即返回,不需要等到消费者线程来取数据。
- SYNC :取数据的时候,如果没有数据,则会进行阻塞等待。
- TIMED : 取数据的时候,如果没有数据,则会进行超时阻塞等待。
源码分析请参考:Java 并发 — 阻塞队列之LinkedTransferQueue源码分析
3.8 LinkedBlockingDeque
- LinkedBlockingDeque是基于双向链表的双端有界阻塞队列
- 默认使用非公平ReentrantLock实现线程安全
- 默认队列最大长度都为Integer.MAX_VALUE
- 不允许null元素添加
- 双端队列可以用来实现 “窃取算法” ,两头都可以操作队列,相对于单端队列可以减少一半的竞争。
- LinkedBlockingDeque 是基于链表的,因此分析它实际就是分析链表而已,这个和我们前面LinkedBlockingQueue实质是差不多的,只是这里是双端队列,可以两头操作队列(队尾也可以出队,队头也可以入队)。