转载请注明源出处:http://www.cnblogs.com/lighten/p/7517454.html
1.前言
本章介绍并发队列ConcurrentLinkedDeque,这是一个非阻塞,无锁,无界 ,线程安全双端操作的队列。简单说就是ConcurrentLinkedQueue的升级版,在JDK7之后才提供。该队列也不允许空元素,而且size方法并不是常量时间,其需要遍历链表,此时并发修改链表会造成统计size不正确。同样,bulk操作和equal以及toArray方法不保证原子性。
2.ConcurrentLinkedDeque
2.1 实现原理
要明白ConcurrentLinkedDeque的实现原理,ConcurrentLinkedQueue的实现原理是基础。数据结构是对称双向链接的链表结构。通过两个方法最小化可见性写入的次数:1.通过单个CAS推进前面多个节点。2.混合对同一个内存位置可见和不可见的写入。
一个节点定义成包含前置节点pre、数据项item、后置节点next。当节点item!=null的时候视为节点还存活,当通过CAS设置item为null的时候,逻辑上该节点已经从集合中删除了。任何时候显然第一个节点的前置节点为null,其终结任何链表的从活着的结点开始的前置引用,最后一个节点同样的,其终结任何链表从活着的结点开始后面的引用。第一个节点和最后一个节点并不一定活着,而且总是能够相互到达。一个新元素原子添加是通过CAS设置前置引用或后置引用为null入队列。
活跃的结点定义为活着的结点和第一个或最后一个节点(不一定活着)。一个deque有两个节点引用:head和tail。头尾节点只是接近第一个或最后一个节点,第一个节点总是能够通过头结点的前置节点找到,最后一个节点类似。
删除节点有三个层次:
1.逻辑删除:CAS设置item为null,然后变成一个可以断开的结点
2.断开连接:删除一个非活跃节点,最终会被GC回收。断开的节点可能被迭代器无限制地访问。逻辑删除后节点还在链表上,通过第一个节点向后遍历或者通过最后一个节点向前遍历,并不保证结点已被逻辑删除,这样的结点仅能通过一个方向可达。
3.GC断开连接:使得断开连接更彻底,活跃的结点无法到达删除的结点。
当一个节点出队列,会断开所有非活跃节点的引用,自我链接是一个十分有效的方法。遍历的时候可以保证不会遍历到相同的元素,但是不保证能够看到后加入的元素。
2.2 数据结构
就是原理中所说的头尾节点和前后终止节点标志。Node的结构如下:
Node的基本方法如下:
casItem:CAS比较更新Item字段的值
lazySetNext:CAS设置next的值,不需要管原值
casNext:CAS比较更新next的值
lazySetPrev:CAS设置prev的值,不需要管原值
casPrev:CAS比较更新prev的值
2.3 基本操作
添加一个元素,都是通过LinkFirst或LinkLast完成的:
步骤如下描述:1.非空校验,创建节点。2.循环设置:头插入先找到当前头结点:1.判断是否被插队2个,是就重新找到头结点。2.判断当前头结点是否是前置终止符,是重新循环。3.p是头结点,设置新节点的next节点为p,再通过CAS设置p的前置节点,被插队失败就继续循环,成功时如果p不等于h意味着2个节点了,需要替换头结点。而后返回。这个步骤之前的队列中也介绍过类似的,并不是很难理解。唯一有疑问的就是PREV_TERMINATOR是什么时候设置的,在链表的构成时候并不存在该节点。注意,插入步骤并没有关心逻辑删除的结点,只关注了终止结点,即前终止结点的特点是其前置节点是其本身,后终止结点是后置节点是其本身。所以可能出现A-null-B的情况,当然从一端进另一端出是不会出现这种情况的,这是发生在一端入一端出。
取出一个元素:
过程很简单:先要找到第一个元素且不为null,取出该元素,该元素必须是活着的item不为null,并且本线程成功将其设置成null,才算数,断开这个节点。如果上面那边替换失败就意味着该节点已经被取出了,重新找其后继节点来处理。看似这个步骤没问题,但是仔细想想就会发现有些不对:一个队列中有AB元素,A元素出队列,1线程让A元素出队里失败被2线程抢先,找到A元素的后继节点是B,但是此刻又有3线程添加了C元素在B的前面,按照栈的后入先出的原则,C应该是要先出队列的,可是顺序却变成了B先出队列。所以这里要注意:push和pop作为堆栈使用,顺序可能会有问题(这个只是根据源码进行的推测,可能疏忽了什么导致推测有错,如果有错请在评论中留言)。这里要注意,由于整个队列可能出现A-null-B这种情况,实际上poll操作也不关心逻辑删除,遇到逻辑删除就跳过找下一个就好了,因为本身就可能是其它线程先取走了。整个顺序是不会改变的,把队列中出现的item为null的结点当做没看见就可以了,整个移除处理就是在unlink方法。
unlink方法有些长,这里直接说下其作用:如果取出节点是头结点,就移除其后面所有连续的逻辑删除节点(如A-null-null-B,unlinkA节点会定位到B最后丢弃中间的变成null-B,更新头尾和前置的终结标志,还有其它可能不再描述),如果取出的结点是尾结点,操作和头结点类似。如果是中间就找到前后非null的结点,都跳过,最后设置前后终结标志。
大体的实现过程就是这样,其它方法不描述。使用也就像一般的队列一样使用就可以了。