Java线程安全的集合之队列
推荐阅读:
一、Queue概要
1. 队列概述
Collection
的子接口。表示队列FIFO(First In First Out)
;队列是一种特殊的线性表,它只允许在表的前端进行删除操作,而在表的后端进行插入操作。
Java 1.8 API :
队列通常(但不一定)以 FIFO(先出先出)的方式对元素排序。例外情况包括优先级队列,这些队列根据提供的比较器对元素进行排序,或者元素的自然排序,以及对元素 LIFO(最后一个先出)排序的 LIFO 队列(或堆栈)。无论使用何种顺序,队列的主管都是该元素,该元素将通过调用
remove()
或poll()
删除。在 FIFO 队列中,所有新元素都插入到队列的尾部。其他类型的队列可能使用不同的放置规则。每个实现都必须指定其排序属性。
2. 常用方法:
这些方法中的每一种都有两种形式:如果操作失败,则抛出一个异常,另一种返回一个特殊值( null或false ,具体取决于操作)。
- 1.抛出异常
方法 | 描述 |
---|---|
boolean add (E e) |
顺序添加一个元素(到达上限后,再添则会抛出异常) |
E remove() |
获得第一个元素并移除(如果队列没有元素时,则抛出异常) |
E element() |
获得第一个元素但不移除(如果队列没有元素时,则会抛出异常) |
- 2.返回特殊值:推荐使用
方法 | 描述 |
---|---|
boolean offer(E e) |
顺序添加一个元素(到达上限后 ,再添加则会返回fals) |
E poll() |
获得第一个元素并移除(如果队列没有元素时,则返回null) |
E peek() |
获得第一元素但不移除(如果队列没有元素时,则返回null) |
二、Queue接口的继承接口和实现类
所有已知的实现类
1. ConcurrentLinkedQueue接口
 基于链接节点的无界线程安全队列。此队列订购元素 FIFO(先出先)。队列的头部是队列中时间最长的元素。队列的尾部是队列中时间最短的元素。新元素插入到队列的尾部,队列检索操作获取队列头处的元素。当许多线程共享对公共集合的访问时,ConcurrentLinkedQueue
是适当的选择。与大多数其他并发集合实现一样,此类不允许使用null
元素。
- Java编程中,线程安全、可高效读写的队列。高并发下性能最好的队列。
一般来说,线程安全都必须加锁,而加锁就不支持高并发,进而无法实现高效率,但是对于ConcurrentLinkedQueue
而言,两者兼备,怎么做到的呢?那就是它的线程安全不是通过锁实现的!
1.1 没有锁也能保证线程安全?
ConcurrentLinkedQueue
是无锁的,使用的是CAS比较交换算法,修改的方法包括三个核心参数(V、E、N)- CAS比较交换算法频繁的使用于操作系统之中,对内存进行管理的时候一般不会通过加锁包保证线程安全(效率低),而是使用这种高效的方式保证线程安全。
1.2 CAS算法简介:
比较交换算法:该算法将存储器位置的内容与给定值进行比较,并且只有它们相同时,才将该存储器位置的内容修改为给定的新值。这是作为单个原子操作完成的。原子性保证了新值是根据最新信息计算出来的; 如果在此期间该值已被另一个线程更新,则写入将失败。操作的结果必须表明它是否进行了替换; 这可以通过简单的布尔响应(此变体通常称为比较和设置),或通过返回从内存位置读取的值(而不是写入它的值)来完成。
CAS操作有3个参数:
V:待更新的变量
E:预期值
N:新值
概念实在读不懂…不用着急,有举例
看图说话:
这里海绵宝宝和狗仔为两个线程,美眉是操作系统,两个线程准备往数组下标为4的位置存值;海绵宝宝准备存"D",而狗仔需要存"d",于是两个人需要问一下美眉4号是不是为null
(null
为两个线程的预期值),如果与线程的预期值一致,两个人分别开始往里面存值;
存值一定有先后(谁先拿到OS的时间片谁先执行本线程,这里类比到达的先后),若海绵宝宝先到达,发现与预期的一样,里面是null
,于是把"D"存进数组的下标4的地方;那么对于狗仔说,当他到达准备放入"d"时,发现预期值变了,不是null
,很难过的回家了,记住了里面的值之后,再次询问美眉,4号里面是"D"吗?美眉说“是!”,当狗仔到达后发现4号里的值与预期的一样,因此,就把"D"替换成了"d",如果还不是,继续循环下去;
即:
- 当只有
V==E
时,V=N;否则表示已经被更新过,则取消当前操作; - 整个过程没有锁,但是改变值的最终只有一个线程,保证了线程的安全性;
1.3 源码
对于CAS算法这部分的源码是无法看到的,这部分源码被包含在了一个Java没有公开开源的底层源码,是用其他语言写的,读者可以通过其他渠道获取(openJDK论坛);
1.4 使用
public static void main(String[] args){
Queue<String> queue = new ConcurrentLinkedQueue<String>();
queue.offer("A"); //插入"A"
queue.offer("B");//插入"B"
queue.poll();//删除"A"
queue.poll();//获得"B"
}
2. BlockingQueue接口
2.1 方法
基本方法
抛出异常 | 特殊值 | 阻塞 | 超时 | |
---|---|---|---|---|
添加 | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
删除 | remove() |
poll() |
take() |
poll(time, unit) |
检查 | element() |
peek() |
不可用 | 不可用 |
无限期等待方法
方法 | 描述 |
---|---|
void put(E e) |
将指定元素插入此队列中,如果没有可用空间,则等待 |
E take() |
获取并移除此队列头部元素,如果没有可用元素,增等待 |
2.2 生产者消费者模式
通过常用方法我们可以了解到这是一种生产者和消费者模式,一个存一个取,因此该接口的一大特点是
- 可以解决生产者、消费者问题
关于生产者消费者模式类比如下图;详细介绍戳下面的链接:
2.3 BlockingQueue实现类
以下实现类的方法与接口用法相似;
ArrayBlockingQueue
- 数组结构实现,有界队列(用户手动固定上限)
public class TestQueue {
public static void main(String[] args){
BlockingQueue<String> objects = new ArrayBlockingQueue<String>(10);//接口引用指向实现类对象
}
LinkedBlockingQueue
- 链表结构实现,无界队列。(默认上限
Integer.MAX_VALUE
)
public class TestQueue {
public static void main(String[] args){
BlockingQueue<String> objects = new ArrayBlockingQueue<String>();//接口引用指向实现类对象
}