【JUC阻塞队列】ConcurrentLinkedQueue

目录

1 前言

2 队列结构

3 入队——offer()方法

3.1 入队过程

3.2 offer()源码

3.3 tail并非一直指向尾结点的意图

4 出队——poll()方法

4.1 出队过程

4.2 poll()源码

5 其它方法

5.1 size()


1 前言

本人jdk版本1.8。在并发编程中,有时候需要使用线程安全的队列。如果要实现一个线程安全的队列有两种方式:

  • 使用阻塞算法:使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁如ArrayBlockingQueue)或两个锁(入队和出队用不同的锁如LinkedBlockingQueue)等方式来实现。
  • 使用非阻塞算法:非阻塞的实现方式则可以使用循环CAS的方式来实现。

ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用FIFO的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法(即CAS算法)来实现,该算法在Michael&Scott算法上进行了一些修改。

2 队列结构

ConcurrentLinkedQueuehead节点和tail节点组成,每个节点Node由节点元素item和指向下一个节点next的引用组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。默认情况下head节点存储的元素为空,tail节点等于head节点。

    private static class Node<E> {
        volatile E item;            // 数据
        volatile Node<E> next;        // 后继节点

        Node(E item){...}

        // cas的修改节点item属性,若节点的item为cmp,则设置为val
        boolean casItem(E cmp, E val) {...}

        // cas的修改节点的next属性
        boolean casNext(Node<E> cmp, Node<E> val) {...}
    }

    private transient volatile Node<E> head;        // 队列“头”指针
        
    private transient volatile Node<E> tail;        // 队列“尾”指针

3 入队——offer()方法

3.1 入队过程

下图是元素入队的过程,初始时head=tail=null

  • 添加元素1:队列更新head节点的next节点为元素1节点。又因为tail节点默认情况下等于head节点,所以它们的next节点都指向元素1节点。
  • 添加元素2:队列首先设置元素1节点的next节点为元素2节点,然后更新tail节点指向元素2节点。
  • 添加元素3:设置tail节点的next节点为元素3节点。
  • 添加元素4:设置元素3next节点为元素4节点,然后将tail节点指向元素4节点。

通过上图我们发现,入队主要做两件事情:

  • 将入队节点设置成当前队列尾节点的下一个节点。
  • 更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点;如果tail节点的next节点为空,则将入队节点设置成tailnext节点,所以tail节点不总是尾节点

3.2 offer()源码

结合上面入队过程,看看jdk源码:

    public boolean offer(E e) {
        checkNotNull(e);                        // 若e为空,抛出NullPointerException
        final Node<E> newNode = new Node<E>(e);

        for (Node<E> t = tail, p = t;;) {        // 指针p用来寻找尾节点
            Node<E> q = p.next;
            if (q == null) {
                // p.next == null 说明p是尾结点,使用casNext令p.next = newNode
                if (p.casNext(null, newNode)) {
                    // 若初始时t不指向尾节点,p=t。因为q!=null,经过几次循环,p向后移动指向了尾结点,故p!=t
                    if (p != t)                   // 若tail没有指向尾节点
                        casTail(t, newNode);      // 令tail指向尾结点。失败了没事
                    return true;
                }
                // 执行到这里说明CAS操作中输给了其它线程,再读p.next
            }
            else if (p == q)
                // 多线程操作时候,由于poll时候会把旧的head变为自引用,然后将head的next设为新的head。
                // 所以这里需要重新找新的head,因为新的head后面的节点才是激活的节点
                p = (t != (t = tail)) ? t : head;
            else
                // 一般情况下令p = p.next,以此来寻找尾结点
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

可以看到castTail方法的调用是有条件的,即p!=t,tail没有指向尾结点。上面源代码主要做了两件事:

  • 第一是定位出尾结点。
  • 第二十使用CAS算法将入队节点设置成尾结点的next节点,如不成功则重试。

3.3 tail并非一直指向尾结点的意图

让tail节点永远作为队列的尾节点,这样实现代码量非常少,而且逻辑非常清楚和易懂。但是这么做有个缺点就是每次都需要使用循环CAS更新tail节点。如果能减少CAS更新tail节点的次数,就能提高入队的效率。

在JDK 1.7的实现中,doug lea使用hops变量来控制并减少tail节点的更新频率,并不是每次节点入队后都将 tail节点更新成尾节点,而是当tail节点和尾节点的距离大于等于常量HOPS的值(默认等于1)时才更新tail节点,tail和尾节点的距离越长使用CAS更新tail节点的次数就会越少,但是距离越长带来的负面效果就是每次入队时定位尾节点的时间就越长,因为循环体需要多循环一次来定位出尾节点,但是这样仍然能提高入队的效率,因为从本质上来看它通过增加对volatile变量的读操作(循环查找尾结点时需要读取tail)来减少了对volatile变量的写操作(tail一直指向尾结点时每次添加元素都要令tail = 添加元素),而对volatile变量的写操作开销要远远大于读操作,所以入队效率会有所提升。

在JDK 1.8的实现中,tail的更新时机是通过p和t是否相等来判断的,其实现结果和JDK 1.7相同,即当tail节点和尾节点的距离大于等于1时,更新tail。

4 出队——poll()方法

4.1 出队过程

并不是每次出队时都更新head节点,当head节点里有元素时,直接弹出head节点里的元素,而不会更新head节点。只有当head节点里没有元素时,出队操作才会更新head节点。采用这种方式也是为了减少使用CAS更新head节点的消耗,从而提高出队效率。

4.2 poll()源码

    public E poll() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                E item = p.item;

                if (item != null && p.casItem(item, null)) {
                    // 只有初始head不指向头结点时才更新head
                    if (p != h) // hop two nodes at a time
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }    // p.next == null说明队列为空,返回null
                else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                }
                else if (p == q)
                    continue restartFromHead;
                else    // p = p.next
                    p = q;
            }
        }
    }

5 其它方法

5.1 size()

没有加锁的情况下将整个队列遍历一遍来计算队列中元素个数,显然在并发场景下计算结果可能不准确。

    public int size() {
        int count = 0;
        for (Node<E> p = first(); p != null; p = succ(p))
            if (p.item != null)
                // Collection.size() spec says to max out
                if (++count == Integer.MAX_VALUE)
                    break;
        return count;
    }

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值