并发队列ConcurrentLinkedQueue的实现原理

目录

一、介绍

二、ConcurrentLinkedQueue的结构

三、入队过程

1. 入队过程

2. 设置tail节点

3. 入队源码

四、出队过程

1. 出队过程

2. 更新head节点

3. 出队源码

五、使用实例

六、参考资料


一、介绍

        java.util.concurrent.ConcurrentLinkedQueue是基于FIFO的单向链表的无界线程安全队列。添加元素到队列尾部;获取返回队列头部的元素。

        线程安全一般采用两种方式:阻塞和非阻塞。阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现。非阻塞算法的队列可以使用循环CAS的方式。ConcurrentLinkedQueue采用了非阻塞方式实现

二、ConcurrentLinkedQueue的结构

        如下图所示。ConcurrentLinkedQueue由首节点Node<E> head、尾节点Node<E> tail组成,都用volatile修饰。而Node则由元素E item、下一节点Node<E> next组成,都用volatile修饰。

        ConcurrentLinkedQueue是由Node的next关联起来形成链表结构的队列。默认head节点元素为null,而tail等于head,即:head = tail = new Node<E>(null)

三、入队过程

1. 入队过程

        入队列就是将元素添加到队列的尾部。如上图所示,是元素入队过程:

  • 添加元素1:更新head节点的next节点为元素1节点。同时tail的next节点都指向元素1节点。
  • 添加元素2:设置元素1节点的next节点为元素2节点;更新tail节点指向元素2节点。
  • 添加元素3:设置tail的next节点为元素3节点。
  • 添加元素4:设置元素3的next节点为元素4节点;更新tail节点指向元素4节点。 

        发现入队主要做两件事情:第一是将入队节点设置成当前队列尾节点的next节点;第二是更新tail节点。当前队列尾节点不一样是tail节点,即:tail节点不总是尾节点,详见下《设置tail节点》。

2. 设置tail节点

        tail节点不总是队列的尾节点,减少自旋CAS设置尾节点来提高入队效率

更新tail节点的流程图

3. 入队源码

        注意:入队永远返回true,原因是通过自旋CAS操作完成,所以不要通过返回值判定入队是否成功

/**
 * 添加元素
 */
public boolean add(E e) {
    return offer(e);
}
// 入队到链表尾
public boolean offer(E e) {
    // 元素不能为null
    checkNotNull(e);
    // 元素构建成Node
    final Node<E> newNode = new Node<E>(e);

    // 自旋方式,添加到队列尾部
    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        // tail的next为null,说明到链表尾部了(tail指向最后一个元素),就入队
        if (q == null) {
            // CAS更新入队节点设置为tail的next,若不成功就自旋
            if (p.casNext(null, newNode)) {
                // 如果p不等于t,说明其它线程更新tail, 也就不会走到q==null这个分支了,p取到的可能是t后面的值
                if (p != t) // hop two nodes at a time
                    casTail(t, newNode);  // Failure is OK.
                return true; // 入队成功
            }
        }
        // 如果p的next等于p,说明p已经被删除了(已经出队了),重新设置p的值
        else if (p == q)
            p = (t != (t = tail)) ? t : head;
        // t后面还有值,重新设置p的值
        else
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

四、出队过程

1. 出队过程

        出队列就是从队列里返回一个元素。如上图所示,是元素出队过程。主要做两件事情:第一是队列移除出队元素的Node,第二是更新head节点。注意:并不是每次出队都更新head节点,详见下节《更新head节点》

2. 更新head节点

        并不是每次出队都更新head节点,原因是减少CAS更新来提高出队效率

更新head流程图

3. 出队源码

/**
 * 弹出元素
 */
public E poll() {
    restartFromHead:
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            // head的元素
            E item = p.item;

            // head不为null,直接弹出head
            if (item != null && p.casItem(item, null)) {
                // 其他线程设置head,导致head变化,则重新更新head
                if (p != h) // hop two nodes at a time
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            // head为null,弹出head的next节点并更新head
            else if ((q = p.next) == null) {
                updateHead(h, p);
                return null;
            }
            // 如果p等于p,说明p已经出队了,重试
            else if (p == q)
                continue restartFromHead;
            else
                p = q;
        }
    }
}

// CAS更新head
final void updateHead(Node<E> h, Node<E> p) {
    if (h != p && casHead(h, p))
        h.lazySetNext(h);
}

五、使用实例

@Test
public void concurrentLinkedQueueTest(){
    ConcurrentLinkedQueue<Integer> concurrentLinkedQueue = new ConcurrentLinkedQueue<>();
    // 添加元素
    for (int i = 0; i < 10; i++) {
        concurrentLinkedQueue.add(i);
    }
    
    Object[] array = concurrentLinkedQueue.toArray();
    for (int i = 0; i < array.length; i++) {
        // 具有顺序性
        System.out.println(array[i]);
    }
}

六、参考资料

深入ConcurrentLinkedQueue底层原理与源码解析 - 知乎

Java并发编程-并发队列(ConcurrentLinkedQueue)的原理分析_记忆力不好的博客-CSDN博客_concurrentqueue 原理

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值