ConcurrentLinkedQueue原理(上)

本文深入分析了并发队列ConcurrentLinkedQueue的设计原理与实现细节,包括其数据结构、非阻塞算法、操作方法及性能优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ConcurrentLinkedQueue是Queue的一个线程安全实现。<wbr style="line-height:25px"><br style="line-height:25px"> 它是一个基于链接节点的无界线程安全队列。此队列按照FIFO(先进先出)原则对元素进行排序。队列的头部是队列中时间最长的元素。<br style="line-height:25px"> 队列的尾部是队列中时间最短的元素。新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。<br style="line-height:25px"> 当多个线程共享访问一个公共collection时,ConcurrentLinkedQueue是一个恰当的选择。此队列不允许使用null元素。<br style="line-height:25px"> 我来分析设计一个线程安全的队列哪几种方法。<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">第一种:使用synchronized同步队列</wbr></span><wbr style="line-height:25px">,就像Vector或者Collections.synchronizedList/Collection那样。<br style="line-height:25px"> 显然这不是一个好的并发队列,这会导致吞吐量急剧下降。<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">第二种:使用Lock</wbr></span><wbr style="line-height:25px">。一种好的实现方式是使用ReentrantReadWriteLock来代替ReentrantLock提高读取的吞吐量。<br style="line-height:25px"> 但是显然ReentrantReadWriteLock的实现更为复杂,而且更容易导致出现问题,<br style="line-height:25px"> 另外也不是一种通用的实现方式,因为ReentrantReadWriteLock适合哪种读取量远远大于写入量的场合。<br style="line-height:25px"> 当然了ReentrantLock是一种很好的实现,结合Condition能够很方便的实现阻塞功能,<br style="line-height:25px"> 这在后面介绍BlockingQueue的时候会具体分析。<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">第三种:使用CAS操作</wbr></span><wbr style="line-height:25px">。尽管Lock的实现也用到了CAS操作,但是毕竟是间接操作,而且会导致线程挂起。<br style="line-height:25px"> 一个好的并发队列就是采用某种非阻塞算法来取得最大的吞吐量。<br style="line-height:25px"> ConcurrentLinkedQueue采用的就是第三种策略。<br style="line-height:25px"> 它采用了参考资料1(<a target="_blank" rel="nofollow" href="http://www.cs.rochester.edu/u/scott/papers/1996_PODC_queues.pdf" style="color:rgb(207,121,28); line-height:25px; text-decoration:none">http://www.cs.rochester.edu/u/scott/papers/1996_PODC_queues.pdf</a><wbr style="line-height:25px">)中的算法。<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">要使用非阻塞算法来完成队列操作,那么就需要一种“循环尝试”的动作,就是循环操作队列,直到成功为止,失败就会再次尝试。</wbr></span><wbr style="line-height:25px"><br style="line-height:25px"> 针对各种功能深入分析。<br style="line-height:25px"> 先介绍下ConcurrentLinkedQueue的数据结构。<br style="line-height:25px"> ConcurrentLinkedQueue只有头结点、尾节点两个元素,而对于一个节点Node而言除了保存队列元素item外,还有一个指向下一个节点的引用next。<br style="line-height:25px"> 看起来整个数据结构还是比较简单的。但是也有几点是需要说明:<br style="line-height:25px"> 1.<span style="line-height:25px"><wbr style="line-height:25px">所有结构(head/tail/item/next)都是volatile类型</wbr></span><wbr style="line-height:25px">。这<span style="line-height:25px"><wbr style="line-height:25px">是因为ConcurrentLinkedQueue是非阻塞的,<br style="line-height:25px"> 所以只有volatile才能使变量的写操作对后续读操作是可见的</wbr></span><wbr style="line-height:25px">(这个是有happens-before法则保证的)。同样也不会导致指令的重排序。<br style="line-height:25px"> 2.<span style="line-height:25px"><wbr style="line-height:25px">所有结构的操作都带有原子操作,这是由AtomicReferenceFieldUpdater保证的</wbr></span><wbr style="line-height:25px">,<br style="line-height:25px"> 这在原子操作中介绍过。它能保证需要的时候对变量的修改操作是原子的。<br style="line-height:25px"> 3.<span style="line-height:25px"><wbr style="line-height:25px">由于队列中任何一个节点(Node)只有下一个节点的引用,所以这个队列是单向的,根据FIFO特性,也就是说出队列在头部(head),入队列在尾部(tail)。</wbr></span><wbr style="line-height:25px"><br style="line-height:25px"> 头部保存有进入队列最长时间的元素,尾部是最近进入的元素。<br style="line-height:25px"> 4.<span style="line-height:25px"><wbr style="line-height:25px">没有对队列长度进行计数</wbr></span><wbr style="line-height:25px">,所以队列的长度是无限的,同时获取队列的长度的时间不是固定的,这需要遍历整个队列,并且这个计数也可能是不精确的。<br style="line-height:25px"> 5.<span style="line-height:25px"><wbr style="line-height:25px">初始情况下队列头和队列尾都指向一个空节点,但是非null</wbr></span><wbr style="line-height:25px">,这是为了方便操作,<span style="line-height:25px"><wbr style="line-height:25px">不需要每次去判断head/tail是否为空</wbr></span><wbr style="line-height:25px">。但是head却不作为存取元素的节点,<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">tail在不等于head情况下保存一个节点元素</wbr></span><wbr style="line-height:25px">。也就是说head.item这个应该一直是空,但是tail.item却不一定是空(如果head!=tail,那么tail.item!=null)。<br style="line-height:25px"> 对于第5点,可以从ConcurrentLinkedQueue的初始化中看到。这种头结点也叫“<span style="line-height:25px"><wbr style="line-height:25px">伪节点</wbr></span><wbr style="line-height:25px">”,也就是说它不是真正的节点,只是一标识,就像c中的字符数组后面的\0以后,只是用来标识结束,并不是真正字符数组的一部分。<br style="line-height:25px"> privatetransientvolatileNode&lt;E&gt;head=newNode&lt;E&gt;(null,null);<br style="line-height:25px"> privatetransientvolatileNode&lt;E&gt;tail=head;<br style="line-height:25px"> 有了上述5点再来解释相关API操作就容易多了。<br style="line-height:25px"> 在上一节中列出了add/offer/remove/poll/element/peek等价方法的区别,所以这里就不再重复了。<br style="line-height:25px"> 清单1入队列操作<br style="line-height:25px"><span style="color:#3366ff; line-height:25px"></span><span style="color:#993300; line-height:25px">publicboolean</span><span style="color:#3366ff; line-height:25px"></span><span style="color:#ff6600; line-height:25px">offer</span><span style="color:#3366ff; line-height:25px">(Ee){<br style="line-height:25px"> if(e==null)thrownewNullPointerException();<br style="line-height:25px"> Node&lt;E&gt;n=newNode&lt;E&gt;(e,null);<br style="line-height:25px"> for(;;){<br style="line-height:25px"> Node&lt;E&gt;t=tail;<br style="line-height:25px"> Node&lt;E&gt;s=t.getNext();<br style="line-height:25px"> if(t==tail){<br style="line-height:25px"> if(s==null){<br style="line-height:25px"> if(t.casNext(s,n)){<br style="line-height:25px"> casTail(t,n);<br style="line-height:25px"> returntrue;<br style="line-height:25px"> }<br style="line-height:25px"> }else{<br style="line-height:25px"> casTail(t,s);<br style="line-height:25px"> }<br style="line-height:25px"> }<br style="line-height:25px"> }<br style="line-height:25px"> }</span><br style="line-height:25px"> 清单1描述的是入队列的过程。整个过程是这样的。<br style="line-height:25px"> 1.获取尾节点t,以及尾节点的下一个节点s。如果尾节点没有被别人修改,也就是t==tail,进行2,否则进行1。<br style="line-height:25px"> 2.如果s不为空,也就是说此时尾节点后面还有元素,那么就需要把尾节点往后移,进行1。否则进行3。<br style="line-height:25px"> 3.修改尾节点的下一个节点为新节点,如果成功就修改尾节点,返回true。否则进行1。<br style="line-height:25px"> 从操作3中可以看到是先修改尾节点的下一个节点,然后才修改尾节点位置的,所以这才有操作2中为什么获取到的尾节点的下一个节点不为空的原因。<br style="line-height:25px"> 特别需要说明的是,对尾节点的tail的操作需要换成临时变量t和s,一方面是为了去掉volatile变量的可变性,另一方面是为了减少volatile的性能影响。<br style="line-height:25px"><br style="line-height:25px"> 清单2描述的出队列的过程,这个过程和入队列相似,有点意思。<br style="line-height:25px"> 头结点是为了标识队列起始,也为了减少空指针的比较,所以头结点总是一个item为null的非null节点。<br style="line-height:25px"> 也就是说head!=null并且head.item==null总是成立。所以实际上获取的是head.next,<br style="line-height:25px"> 一旦将头结点head设置为head.next成功就将新head的item设置为null。至于以前就的头结点h,h.item=null并且h.next为新的head,<br style="line-height:25px"> 但是由于没有对h的引用,所以最终会被GC回收。这就是整个出队列的过程。<br style="line-height:25px"> 清单2出队列操作<br style="line-height:25px"><span style="color:#3366ff; line-height:25px"></span><span style="color:#993300; line-height:25px">public</span><span style="color:#3366ff; line-height:25px">E</span><span style="color:#ff6600; line-height:25px">poll</span><span style="color:#3366ff; line-height:25px">(){<br style="line-height:25px"></span><span style="color:#993300; line-height:25px">for</span><span style="color:#3366ff; line-height:25px">(;;){<br style="line-height:25px"> Node&lt;E&gt;h=head;<br style="line-height:25px"> Node&lt;E&gt;t=tail;<br style="line-height:25px"> Node&lt;E&gt;first=h.getNext();<br style="line-height:25px"></span><span style="color:#993300; line-height:25px">if</span><span style="color:#3366ff; line-height:25px">(h==head){<br style="line-height:25px"> if(h==t){<br style="line-height:25px"> if(first==null)<br style="line-height:25px"> returnnull;<br style="line-height:25px"> else<br style="line-height:25px"> casTail(t,first);<br style="line-height:25px"> }</span><span style="color:#993300; line-height:25px">elseif</span><span style="color:#3366ff; line-height:25px">(casHead(h,first)){<br style="line-height:25px"> Eitem=first.getItem();<br style="line-height:25px"> if(item!=null){<br style="line-height:25px"> first.setItem(null);<br style="line-height:25px"> returnitem;<br style="line-height:25px"> }<br style="line-height:25px"> //elseskipoverdeleteditem,continueloop,<br style="line-height:25px"> }<br style="line-height:25px"> }<br style="line-height:25px"> }<br style="line-height:25px"> }<br style="line-height:25px"></span><br style="line-height:25px"> 另外对于清单3描述的获取队列大小的过程,由于没有一个计数器来对队列大小计数,所以获取队列的大小只能通过从头到尾完整的遍历队列,显然这个代价是很大的。所以通常情况下ConcurrentLinkedQueue需要和一个AtomicInteger搭配才能获取队列大小。后面介绍的BlockingQueue正是使用了这种思想。<br style="line-height:25px"> 清单3遍历队列大小<br style="line-height:25px"><span style="color:#3366ff; line-height:25px"></span><span style="color:#993300; line-height:25px">publicint</span><span style="color:#3366ff; line-height:25px">size(){<br style="line-height:25px"> intcount=0;<br style="line-height:25px"></span><span style="color:#993300; line-height:25px">for</span><span style="color:#3366ff; line-height:25px">(Node&lt;E&gt;p=first();p!=null;p=p.getNext()){<br style="line-height:25px"></span><span style="color:#993300; line-height:25px">if</span><span style="color:#3366ff; line-height:25px">(p.getItem()!=null){<br style="line-height:25px"></span><span style="color:#808080; line-height:25px">//Collections.size()specsaystomaxout</span><br style="line-height:25px"><span style="color:#3366ff; line-height:25px">if(++count==Integer.MAX_VALUE)</span><br style="line-height:25px"><span style="color:#3366ff; line-height:25px"></span><span style="color:#993300; line-height:25px">break</span><span style="color:#3366ff; line-height:25px">;</span><br style="line-height:25px"><span style="color:#3366ff; line-height:25px">}</span><br style="line-height:25px"><span style="color:#3366ff; line-height:25px">}</span><br style="line-height:25px"><span style="color:#3366ff; line-height:25px"></span><span style="color:#993300; line-height:25px">return</span><span style="color:#3366ff; line-height:25px">count;</span><br style="line-height:25px"><span style="color:#3366ff; line-height:25px">}</span><br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">注意1:</wbr></span>关于ConcurrentLinkedQueue原理更多可参考《<strong><a title="阅读全文" target="_blank" href="http://hubingforever.blog.163.com/blog/static/171040579201062951055462/" style="color:rgb(207,121,28); line-height:25px; text-decoration:none">ConcurrentLinkedQueue原理(下)</a></strong></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr>
注意2:关于ConcurrentLinkedQueue的API介绍可参考《ConcurrentLinkedQueue
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值