ConcurrentLinkedQueue源码解析
ConcurrentLinkedQueue是一个基于链表节点Node的无界线程安全队列。它是一个先进先出队列,头结点是在这个队列中最久的节点,尾节点是在这个队列中最新的节点。每次插入新节点都从尾部插入,获取节点就从头部插入。
ConcurrentLinkedQueue使用的是非阻塞的CAS算法,内部是一个链表。
类图
ConcurrentLinkedQueue的类图如下所示:
ConcurrentLinkedQueue实现了Queue接口和继承了AbstractQueue类。Itr和Node则是它的内部类。
1 | public class ConcurrentLinkedQueue<E> extends AbstractQueue<E> |
Queue 接口只是定义了一些队列的公共方法,如下:
1 | public interface Queue<E> extends Collection<E> { |
AbstractQueue也实现了Queue接口,还提供了某些方法的默认实现
1 | // 从这可以发现,Add方法,其实就是调用的offer方法 |
主要属性
1 | /** |
主要方法
无参构造函数
1 | /** |
生成一个值为null的节点,head,tail都指向该节点
有参构造函数
1 | /** |
add方法
1 | /** |
offer方法
将元素添加到队列的队尾
1 | /** |
一步一步看:
- 初始化的时候,新增一个item为null,next为null的node,然后把head和tail指向这个node
- 接着往queue里面新增一个值为a的节点
- 在上述offer方法的第14行和15行,分别生成一个节点p,和q,都指向当前的null Node,生成一个q节点,指向p节点的next,因此为null
- 此时,null == q,那么cas设置p的next为新创建的职位a的node
- 此时 p == t,因此不会进入27行的if判断
- 然后,再向queue中新增一个b节点。
- 这个时候的,p和t,还是指向tail,tail还是指向当前的null节点,但是,新生成的q节点,就指向了a节点
- 此时null != q,p != q,因此会进入49行的else分支
- 49行的else分支做的事情就是寻找新的为节点,会将p指向a节点
- 然后又进入了添加节点a的逻辑,但是,此时,p指向a,tail指向null节点,27行的条件p != t成立,重新把tail指向新的b节点。这就是tail的滞后性
- 重新设置tail的时候,允许失败,在下一次添加的时候,还是会重试的。
什么时候p == q条件会成立呢? 这需要先看看poll方法的逻辑。
poll方法
1 | public E poll() { |
- 队列一开始为空时的状态如图所示:
由于head节点指向的是item为null的哨兵节点,所以会执行到18行的if语句。假设这个过程中没有线程调用offer方法,此时q等于null,这个时候队里的状态如下图所示:
所以会执行updateHead方法,由于h等于p所以没有设置头结点,poll方法直接返回null
1 | /** |
- 如果,执行到18行的if语句时,已经有其他线程调用了offer方法并成功添加一个元素到队列,这时候q指向的是新增元素的节点:
因此,18行if语句不满足条件,继而执行23行的if语句,但p不等于q,所以再执行26行,将p指向了q指向的节点
接着,又进入循环,此时p指向的元素值不为null,尝试将p的item值设置为null,如果此时没有其他线程进行poll操作,则cas成功之后,会执行13行代码,因为此时p != h,所以,设置头结点为p,并设置h的next节点为h自己,poll然后返回被从队列移除的节点值item
这个状态就是offer方法里面p == q时候的状态
- 假设现在一个线程调用了poll操作,那么执行9行代码的时候,队列状态如下所示:
这时候执行18行代码,返回null
- 现在poll的代码还有分支23行还没有执行过。假设线程A执行poll操作时当前队列状态如下:
那么执行p.casItem通过CAS操作尝试设置p的item值为null,假设CAS设置成功则标记该节点并从对了中将其移除
由于p != h,所以会执行updateHead方法,假设线程A执行updateHead前另外一个线程B开始poll操作,这时候线程B的p指向head节点,但是还没有执行到18行代码:
然后线程A执行updateHead操作,执行完毕后,线程A退出
然后线程B继续执行18行代码,q = p.next,由于该节点是自引用节点,所以p == q所以会执行23行代码,跳转到goto label处,获取当前队列头head
总结:poll方法在移除一个元素时,只是简单的使用CAS操作把当前节点的item值设置为null,然后通过重新设置头结点将该元素从队列里面移除,被移除的节点就成了孤立节点,这个节点会在垃圾回收的时候被回收掉。另外,如果在执行分支中发现头肩点被修改了,就要跳到外层循环重新获取新的节点
peek方法
1 | public E peek() { |
第一次调用peek方法的时候会删除哨兵节点,并让队列中的head节点指向队列中的第一个元素,或者null。
当队列为空时
第一次执行peek的时候,null == item,然后在第6行代码出,执行q = p.next,这时候q节点指向的才是队列里面第一个真正的元素,或者队列为null则q指向null。
这时候执行updateHead,由于h == p,所以不进行任何操作,然后peek操作会返回null。
当队列中至少有一个元素时
这时执行13行代码,p指向了q节点,然后执行代码6行
执行第7行代码时,发现item不为null,所以执行updateHead方法,由于h != p,所以设置头结点
size方法
1 | /** |
在计算的时候没有加锁,所以从调用size函数到返回结果期间有可能增删元素,导致统计元素的个数不准确。
remove方法
1 | /** |
如果队列里存在该元素则删除该元素,如果存在多个则删除第一个,并返回true,否则返回false。
contains方法
1 | /** |
判断队列里面是否含有指定对象,由于是遍历整个队列,所以像size操作一样结果也不是那么精确,有可能调用该方法时元素还在队列里面,但是遍历过程中其他线程才把该元素删除了,那么就会返回false。