ConcurrentLinkedQueue总结——《Java并发编程之美》

ConcurrentLinkedQueue

JDK中提供了一系列场景的并发安全队列。总的来说,按照事先方式的不同可分为阻塞队列和非阻塞队列,前者使用锁实现,后者则使用CAS非阻塞算法实现。

ConcurrentLinkedQueue是线程安全的无解非阻塞队列,其底层数据结构使用单向链表实现,对于入队和出队操作使用CAS来实现线程安全。

在这里插入图片描述

ConcurrentLinkedQueue内部的队列使用单向链表方式实现,其中有两个volatile类型的Node节点分别用来存放队列的首、尾节点。从下面的无参构造函数可知,默认头、尾节点都是指向item为null的哨兵节点。新元素会被插入队列末尾,出队时从队列头部获取一个元素。

    public ConcurrentLinkedQueue() {
        head = tail = new Node<E>(null);
    }

其内部则使用UNSafe工具类提供的CAS算法来保证出入队时操作链表的原子性。

1、offer操作

offer是在队列末尾添加一个元素,如果传递的参数是null则抛出NPE异常,否则由于 ConcurrentLinkedQueue是无界队列,该方法一直会返回true。另外,由于使用CAS无阻塞算法,因此该方法不会阻塞挂起调用线程。

public boolean offer(E e) {
    // (1)e为null则抛出空指针异常
    checkNotNull(e);
    // (2)构造Node节点,在构造函数内部调用unsafe.putObject
    final Node<E> newNode = new Node<E>(e);
    // (3)从尾结点进行插入
    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        // (4)如果q==null说明p是尾结点则执行插入
        if (q == null) {
            // p is last node (5) 使用CAS设置p节点的next节点
            if (p.casNext(null, newNode)) {
                // Successful CAS is the linearization point
                // for e to become an element of this queue,                 
                // and for newNode to become "live".
                // (6)CAS成功,则说明新增节点已经被放入链表,然后设置当前尾结点(包含head,第1,3,5.。。个节点为尾结点)
                if (p != t) // hop two nodes at a tim
                    casTail(t, newNode);  // Failure is OK.
                return true;
            }
            // Lost CAS race to another thread; re-read next
        }
        else if (p == q)
            // (7)多线程操作时,由于poll操作移除元素后可能会把head变为自引用,也就是head的next变成了head,所以这里需要找新的head
            // We have fallen off list.  If tail is unchanged, it
            // will also be off-list, in which case we need to
            // jump to head, from which all live nodes are always
            // reachable.  Else the new tail is a better bet.
            p = (t != (t = tail)) ? t : head;
        else
            // Check for tail updates after two hops.
            // (8)寻找尾结点
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

单个线程调用offer方法的运行流程:

  • 首先对传参进行空参检查
  • 执行代码(2)并使用item作为构造函数参数创建一个新的节点
  • 执行代码(3)从队列尾部节点开始循环,打算从队列尾部添加元素
  • 当执行代码(4)时队列状态如图所示,这是p,t,head,tail同时指向了item为null 的哨兵节点,由于哨兵节点的next节点为null,所以这里q也指向null。代码(4)发现q==null则执行代码(5)

在这里插入图片描述

  • 代码(5)通过CAS原子操作判断p节点的next节点是否为unll,如果为null则使用节点newNode替换p的next,然后执行代码(6)
  • 这里由于p==t所以没有设置尾部节点,然后退出offe方法,这是队列的状态如图

在这里插入图片描述

多个线程调用offer方法的运行流程:

  • 假设有两个线程A,B同时执行到代码(5)p.casNext(null, newNode)。由于CAS的比较设置操作是原子性的,所以这里假设线程A先执行了比较设置操作,发现当前p的next节点确实是null,则会原子性的更新next节点为item1,这时候线程B也会判断p的next节点是否为null,结果发现不是null(因为线程A已经设置了p的next节点为item1),则会调到代码(3),然后执行代码(4)

在这里插入图片描述

  • 根据上面状态可知线程B接下来会执行代码(8),然后把q赋予给p,这时候队列状态如图所示

在这里插入图片描述

  • 然后线程再次跳转到(3),当执行到(4)时状态如图

在这里插入图片描述

  • 此时用于q==null,所以线程B会执行代码(5),通过CAS操作判断当前p的next节点是否为null,不是则再次循环尝试,是则用item2替换。假设CAS成功了,那么执行代码(6),由于p!=他,所以设置tail节点为item2,然后退出offer方法

在这里插入图片描述

代码(7)要在poll操作后才会执行。

可见,offer操作中的关键步骤是代码(5),通过原子CAS操作来控制某时只有一个线程可以追加元素到队列末尾。通过CAS竞争失败的线程会通过循环一次次尝试进行CAS操作,知道CAS操作成功才返回,也就是通过使用无限循环不断进行CAS尝试方法来替代阻塞算法挂起调用线程。相比阻塞算法,这是使用CPU原换取阻塞锁带来的开销。

2、add操作

add操作是在链表末尾添加一个元素,其实在内部调用的还是offer操作

    public boolean add(E e) {
        return offer(e);
    }
3、poll操作

poll操作是在队列同步获取并移除一个元素,如果队列为空则返回null。下面看看实现原理

public E poll() {
    // (1)goto标记
    restartFromHead:
    // (2)无限循环
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
         	// (3)保存当前节点值
         	E item = p.item;
         	// (4)当前节点右值则CAS变为NULL
            if (item != null && p.casItem(item, null)) {
                // Successful CAS is the linearization point
                // for item to be removed from this queue.
                // (5)CAS成功则标记当前节点并从链表中移除
                if (p != h) // hop two nodes at a time
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            // (6)当前队列为空则返回null
            else if ((q = p.next) == null) {
                updateHead(h, p);
                return null;
            }
            // (7)如果当前节点被自引用了,则重新寻找新的队列头结点
            else if (p == q)
                continue restartFromHead;
            else
                // (8)
                p = q;
        }
    }
}    
final void updateHead(Node<E> h, Node<E> p) {
    if (h != p && casHead(h, p))
        h.lazySetNext(h);
}

poll方法在移除一个元素时,只是简单地使用CAS操作把当前节点的item值设置为null,然后通过重新设置头结点将该元素从队列里面移除,被移除的节点就变成孤立节点,这个节点会在垃圾回收时被回收掉。另外,如果在执行分支中发现头结点被修改了,要跳到外层循环重新获取新的头结点。

4、peek操作

peek操作是获取队列头部一个元素(只获取不移除),如果队列为空则返回null

public E peek() {
    //(1)
    restartFromHead:
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            // (2)
            E item = p.item;
            // (3)
            if (item != null || (q = p.next) == null) {
                updateHead(h, p);
                return item;
            }
            // (4)
            else if (p == q)
                continue restartFromHead;
            else
                // (5)
                p = q;
        }
    }
}

peek操作的代码结构与poll类似,不同之处在于(3)处少了castItem操作。其实这很正常,因为peek只是获取对流头元素值,并不清空其值。根据前面的介绍我们知道第一次执行offer后head指向的是哨兵节点(也就是item为null的节点),那么第一次执行peek时在代码(3)中会发现item==null,然后执行q = p.next,这时候q节点指向的才是队列里面第一个真正的元素,获取如果队列为null则q指向null。

peek操作的代码与poll代码类似,只是前者指获取对流头元素电商部并不从队列里将它删除,而后者获取后需要从队列里将它删除。另外,在第一次调用peek操作时,会删除哨兵节点,并让队列的head节点指向队列里面第一个元素或者null。

5、size操作

计算当前队列元素个数,在并发环境下不是很有用,因为CAS没有加锁,所以从调用size函数到返回结果期间有可能增删元素,导致统计的元素个数不精确。

6、remove操作

如果队列里面存在该元素则删除该元素,如果存在多个则删除第一个,并返回true,否则返回false

public boolean remove(Object o) {
    // 为空则直接返回false
    if (o != null) {
        Node<E> next, pred = null;
        for (Node<E> p = first(); p != null; pred = p, p = next) {
            boolean removed = false;
            E item = p.item;
            if (item != null) {
                if (!o.equals(item)) {
                    next = succ(p);
                    continue;
                }
                removed = p.casItem(item, null);
            } 
            next = succ(p);
            if (pred != null && next != null) // unlink
	            pred.casNext(p, next);
            if (removed)
                return true;
        }
    }
    return false;
}
7、contains操作

按断队列里面是否含有指定对象,由于是遍历整个队列,所以想size操作一样结果也不精确,有可能调用该方法时元素还在队列里面,但是遍历过程中其它线程才把该元素删除了,那么就会返回false。

小结

ConcurrentLinkedQueue的底层使用单向链表数据结构来把偶才能队列元素,每个元素被包装成一个Node节点。队列靠头、尾节点来维护,创建队列时头、为节点指向一个item为null的哨兵节点。第一次执行peek或者first操作时会把head指向第一个真正的队列元素。由于使用非阻塞CAS算法,所以在计算size时有可能进行了offer、poll或者remove操作,导致计算的元素个数不精确,所以在并发情况下size函数不是很有用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值