前言
ConcurerntLinkedQueue
一个 基于链接节点的无界线程安全的FIFO队列
。和其他大部分并发集合类似,此队列不允许使用null元素。
源码解析
内部数据结构
1 | public class ConcurrentLinkedQueue<E> extends AbstractQueue<E> |
从源码可知,ConcurrentLinkedQueue
继承了AbstractQueue
抽象类并实现了Queue
与 Serializable
接口。其内部数据结构有:
- 内部类Node:Node是ConcurrentLinkedQueue存储结构的基本单元。
- 迭代器类Itr:和大多数集合一样,都使用了迭代器模式,所以其内部提供一个元素遍历器。
头尾节点
- head引用的不变性和可变性
不变性(invariants)
- 所有未删除节点,都能从head通过调用succ()方法遍历可达。
- head头结点不能为null
- head节点的next域不能引用到自身
可变性(Non-invariants)
- head节点的item域可能为null,也可能不为null
- 允许tail滞后于head,即:从head开始遍历队列,不一定能到达tail
- tail 引用的不变性和可变性
不变性(invariants)
- 通过tail调用succ()方法,最后节点总是可达的
- tail节点不能为null
可变性(Non-invariants)
- tail节点的item域可能为null,也可能不为null
- 允许tail滞后于head,即:从head开始遍历队列,不一定能到达tail
- tail节点的next域可以引用到自身
- head引用的不变性和可变性
构造函数
1 | //默认会构造一个 dummy 节点 |
由源码可知,ConcurrentLinkedQueue
提供了两个构造函数:无参构造函数和有参构造函数。
无参构造函数只是简单的构造一个dummy节点,
该结点的item域和next域都为null,然后头尾节点都指向这个dummy节点;
有参构造函数是使用参数集合构造一个ConcurrentLinkedQueue。
add源码
1 | public boolean add(E e) { |
在往队列中添加元素时,会调用add方法,add方法内部会调用offer方法。
offer源码
1 | //在队列的尾部添加元素。因队列是无界的,因此方法不会返回false |
offer函数用于将指定元素插入此队列的尾部。大致逻辑为:
- 判断指定元素是否为空,为空,抛出异常,方法结束
- 在指定元素不为空的情况下, 组装节点
- 采用失败即重试的方式,插入节点。
- 获取尾节点p(p = tail)的next节点q
- 如果
q==null
,说明p节点为最后一个节点,则CAS进行设置p的next节点。设置成功后,如果又必须,则重新设置tail节点。方法结束。 - 如果
p == q
,说明其他线程有调用了updateHead方法
,此时tail指针已fallen off list
,则重新设置p指针,进行再次循环。 - 否则,也重新设置p指针,进行再次循环。
单线程模拟入队操作
队列初始态
由源码可知,队列的初始化时,创建了一个dummy节点,且头尾节点都指向这个dummy节点。
元素10入队
由源码可知,在插入元素10时,由于tail节点的next节点为null,因此直接设置新节点为tail节点的next节点。在设置成功后,因为条件
p != t
不成立,所以tail节点的指针不变。元素20入队
由源码可知,在队列中只有元素10时,tail节点的next节点依然不为null,且条件
p == q
也不成立。所以执行p = (p != t && t != (t = tail)) ? t : q;
,则p = tail.next
。进入第二次循环。二次循环成功后,条件p != t
成立,所以tail节点的指针发送改变。即:hop two nodes at a time
。
poll源码
1 | //从队列的头结点取数据 |
poll函数用于获取并移除此队列的头,如果此队列为空,则返回null。。大致逻辑为:
- 从头结点开始,将头结点赋值给变量p,循环队列
- 如果
p.item != null
且CAS操作成功,则返回 p.item 的值。在返回值之前,需要判断是否需要更新头结点。 - 如果
p.next == null
,则调用updateHead
方法设置头结点,然后返回 - 如果
p == q
,则说明p节点已是删除的头结点,则重新循环。 - 否则,设置
p = p.next
,继续循环。
单线程模拟出队操作
队列初始态
使用上面模拟入队后的队列
首次调用poll函数
由源码可知,在首次调用poll函数时,由于头结点的item域为null,则执行
p = p.next
,然后再次循环。此时,节点的item域不为空,则返回item域值。在返回item域值之前,判断条件p != h == true
,则调用updateHead
方法设置头结点。再次调用poll函数
由源码可知,再次调用poll方法时,头结点的item域不为空,所以直接返回item域的值,又因为条件
p != h = false
,所以头结点不会变更,只是头结点的item域变成了null。
updateHead源码
1 | /** |
该方法的功能是设置头结点。
remove方法
1 | public boolean remove(Object o) { |
此函数用于从队列中移除指定元素的单个实例(如果存在)。大致逻辑为从头结点开始遍历,如果item域值为指定元素,则将对于的item域置为null,否则获取下一节点,循环处理。
first源码
1 | //返回队列中第一个没有被删除的节点,另一个版本的poll/peek方法 |
first函数用于找到链表中第一个存活的结点。
succ方法
1 | /** |
succ方法用于获取结点的下一个结点。如果结点的next域指向自身,则返回head头结点,否则,返回next结点。
模拟remove方法调用
队列初始态
使用上面模拟入队后的队列
执行remove(10)
由源码可知,在调用remove方法时,会先调用first方法获取队列的头元素,在first方法的时候,由于
p.item != null = false
且(q = p.next == null) = false
,所以执行p = p.next
,进入二次循环。在二次循环时,会执行updateHead
设置头结点。所以原头结点被特殊处理,等后续清理。
size方法
1 | /** |
此函数用于返回ConcurrenLinkedQueue的大小,从第一个存活的结点(first)开始,往后遍历链表,当结点的item域不为null时,增加计数,之后返回大小。该方法在多线程情况下,并没有多大用处。
contains方法
1 | public boolean contains(Object o) { |