ConcurrentLinkedQueue深度解析

并编程中,一般需要用到安全的队列,如果要自己实现安全队列,可以使用2种方式:

  1. 加锁,这种实现方式就是我们常说的阻塞队列。
  2. 使用循环CAS算法实现,这种方式实现队列称之为非阻塞队列。

使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环CAS的方式来实现,本节我们就来研究下ConcurrentLinkedQueue是如何保证线程安全的同时又能高效的操作的。

ConcurrentLinkedQueue,根据API解释,ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全的队列,按照先进先出原则对元素进行排序。新元素从队列尾部插入,而获取队列元素,则需要从队列头部获取。

看下ConcurrentLinkedQueue的类图:

从类图可以了解ConcurrentLinkedQueue一个大概,ConcurrentLinkedQueue内部持有2个节点:head头结点,负责出列, tail尾节点,负责入列。而元素节点Node,使用item存储入列元素,next指向下一个元素节点。

private static class Node<E> {
 volatile E item;
 volatile Node<E> next;
 //....
 }
public class ConcurrentLinkedQueue<E> extends AbstractQueue<E> implements Queue<E>, java.io.Serializable {
 private transient volatile Node<E> head;
 private transient volatile Node<E> tail; 
 //....
}

ConcurrentLinkedQueue使用约定:

  1. 不允许null入列
  2. 在入队的最后一个元素的next为null
  3. 队列中所有未删除的节点的item都不能为null且都能从head节点遍历到
  4. 删除节点是将item设置为null,队列迭代时跳过item为null的节点
  5. head和tail节点不一定指向头节点或尾节点,可能存在滞后性

之所以有这奇葩约定,全因ConcurrentLinkedQueue是并发非阻塞队列决定的。我们从源码上看一下ConcurrentLinkedQueue实现过程

入队列:在我们的印象中链表特点:tail节点表示最后一个节点,head节点表示第一个节点,ConcurrentLinkedQueue 跟传统的链表有点区别,在单线程环境下符合传统链表特点,但涉及到多线程环境,ConcurrentLinkedQueue 中的tail节点不一定是最后一个节点,他可能是倒数第二个。所以ConcurrentLinkedQueue判断队尾条件是节点的next为null。

public boolean offer(E e) {
 checkNotNull(e); //为空判断,e为null是抛异常
 final Node<E> newNode = new Node<E>(e); //将e包装成newNode
 for (Node<E> t = tail, p = t;;) { //循环cas,直至加入成功
 //t = p = tail 
 Node<E> q = p.next;
 if (q == null) { //判断p是否为尾节点
 //如果是,p.next = newNode
 if (p.casNext(null, newNode)) {
 //首次添加时,p 等于t,不进行尾节点更新,所以所尾节点存在滞后性 
 //并发环境,可能存添加/删除,tail就更难保证正确指向最后节点。
 if (p != t) 
 //更新尾节点为最新元素
 casTail(t, newNode); 
 return true;
 }
 }
 else if (p == q)
 //当tail不执行最后节点时,如果执行出列操作,很有可能将tail也给移除了 
 //此时需要对tail节点进行复位,复位到head节点
 p = (t != (t = tail)) ? t : head;
 else
 //推动tail尾节点往队尾移动
 p = (p != t && t != (t = tail)) ? t : q;
 }
 }

tail不一定执行最后一个节点,但可以确定最后节点的next节点为null。到这可能朋友问他,并发环境什么情况都有可能,ConcurrentLinkedQueue是怎么保证线程安全的?

我们观察offer方法的设计,

是一个死循环,就是不停使用cas判断直到添加元素入队成功。

for (Node<E> t = tail, p = t;;)

2个cas判断方法:p.casNext(null, newNode) 确保队列在入列时是原子操作;casTail(t, newNode); 确保tail队尾在移动改变时是原子操作,而在并发环境,ConcurrentLinkedQueue入列线程安全考虑具体可分2类:

线程1线程2同时入列:

线程1,线程2不管在offer哪个位置开始并发,他们最终的目的都是入列,也即都需要执行casNext方法, 我们只需要确保所有线程都有机会执行casNext方法,并且保证casNext方法是原子操作即可。casNext失败的线程,可以进入下一轮循环,人品好的话就可以入列,衰的话继续循环

线程1遍历,线程2入列:

ConcurrentLinkedQueue 遍历是线程不安全的, 线程1遍历,线程2很有可能进行入列出列操作, 所以ConcurrentLinkedQueue 的size是变化。换句话说,要想安全遍历ConcurrentLinkedQueue 队列,必须额外加锁。

出队列

出队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用。并不是每次出队时都更新head节点,当head节点里有元素时,直接弹出head节点里的元素,而不会更新head节点。只有当head节点里没有元素时,出队操作才会更新head节点。

public E poll() {
 restartFromHead:
 for (;;) {
 for (Node<E> h = head, p = h, q;;) {
 //入列折腾的tail,那出列折腾的就是head
 E item = p.item;
 //出列判断依据是节点的item=null
 //item != null, 并且能将操作节点的item设置null, 表示出列成功
 if (item != null && p.casItem(item, null)) {
 if (p != h) 
 //一旦出列成功需要对head进行移动
 updateHead(h, ((q = p.next) != null) ? q : p);
 return item;
 }
 else if ((q = p.next) == null) {
 updateHead(h, p);
 return null;
 }
 else if (p == q)
 //第一轮操作失败,下一轮继续,调回到循环前
 continue restartFromHead;
 else
 //推动head节点移动
 p = q;
 }
 }
 }

首先获取head节点的元素,并判断head节点元素是否为空,如果为空,表示另外一个线程已经进行了一次出队操作将该节点的元素取走,如果不为空,则使用CAS的方式将head节点的引用设置成null,如果CAS成功,则直接返回head节点的元素,如果CAS不成功,表示另外一个线程已经进行了一次出队操作更新了head节点,导致元素发生了变化,需要重新获取head节点。如果p节点的下一个节点为null,则说明这个队列为空(此时队列没有元素,只有一个伪结点p),则更新head节点。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值