队列是一种经常被用到的数据结构,比如在生产者消费者场景中,简单则如一个链表实现,复杂则想kafka、rocketmq等等分布式消息队列。本文基于JDK1.8来分析java.util.concurrent包中的ConcurrentLinkedQueue,这个基于链表和CAS的并发优化的队列。
内部类
Node
|
|
相比较LinkedList中内部类Node的简单(参见LinkedList),ConcurrentLinkedQueue的Node则费了不少心思。
head&tail
|
|
构造方法
|
|
add
添加元素不能为null,否则抛出NPE。
这个方法中的逻辑看了半天,才看明白,查了网上的资料发现大多说的也不清不楚,索性整理一下记下来。我们首先看单线程的情况。
第一次添加元素,此时head、tail都指向头结点。
1、取tail的值赋给p、t.
2、执行Node
第二次添加元素,此时head、tail依旧指向头结点。
1、取tail的值赋给p、t.
2、执行Node
3、p==t,所以根据三目运算符,p被赋值为q,这一步结束后p指向了第一次新添加的结点,相当于完成了p指针向尾部元素的推移。
4、开始循环,进入上述第二步,此时q为null,所以照常添加第二个元素,接着判断p是否等于t,我们知道tail此时仍然指向头结点,未曾变化,所以p!=t,这样就完成了tail的更新,tail指向尾结点。
这样,单线程的添加是没有问题了,主要就是设计第一个和第三分支的代码逻辑。我们再分析多线程同时添加元素的情况,此时tail指向尾结点:
1、假设某个线程A在第一个分支q==null进入之后,casNext操作之前,另外一个线程B竞争成功,这样A操作p.casNext(null, newNode)失败。
2、继续循环,执行Node
3、进入第三个分支,p==t,导致p赋值为q,后面的流程就是跟之前的单线程差不多了,可以自行推导。
这样看来第三个分支的作用主要就是,更新变量p:
- 使p指向更新后的tail尾结点。(多线程中)
- 使p一步步的向尾部迁移。(单线程中)
这种CAS乐观锁的方式相比较锁定而言,效率提高不少。
poll
|
|
同样,poll方法也可以分为单线程和多线程两种情况分析。
先看单线程情况,此时假设head指向头结点,item等于null:
1、第一次弹出元素,进入第三个分支,很显然,p开始指向头部第一个元素。
2、进入循环,item!=null成立,单线程情况下p.casItem(item, null)成功,p != h成立,开始执行updateHead方法,具体见上面代码,更新head值,head指向p.next(假设不为null),返回item。
3、第二次弹出,此时item不为null,经过casItem之后,p.item为null,但是此时p依旧等于h,跳过,直接返回item。
我们发现两次弹出元素之后,不是每次都更新head值。这是单线程的情况,多线程无非就是在第一个分支casItem失败之后,执行最后一步p=q,让p挪到下一个结点。
以上是无限并发非阻塞队列ConcurrentLinkedQueue的主要内容,至于还有peek方法是只读取而不删除头部结点,在此不做分析。
-EOF-