霸榜GitHub阿里内部ConcurrentLinkedQueue笔记(源码)有多强?

本文深入分析了Java中的并发安全队列ConcurrentLinkedQueue,探讨了其实现原理,包括其无界、非阻塞、FIFO特性,以及基于CAS的并发操作。文章详细解释了添加元素、获取元素和删除元素的并发操作,并讨论了并发场景下的优化技巧和删除元素的复杂情况。通过对ConcurrentLinkedQueue源码的解析,展示了Doug Lea的精妙设计,如跳跃式更新和节点链接策略,揭示了其在并发性能和线程安全方面的优势。
摘要由CSDN通过智能技术生成

ConcurrentLinkedQueue 源码浅析

队列是一种常见的数据结构,主要特点是 FIFO,Java 为其定义了接口类:Queue,并提供了丰富的实现,有底层基于数组的[有界]队列,也有基于节点链接的无界队列,有阻塞队列,有非阻塞队列,还有并发安全的队列。

常见的队列实现的两种方式:数组、节点链接。

Java 对队列的基本实现在包:java.util 中,对并发安全实现主要存在于 Java 的 JUC 包下。
在使用 Java 的线程池工具:ThreadPoolExecutor,其使用阻塞队列来缓存任务,因为阻塞队列具备通知唤醒的功能,能够在任务添加或消耗时进行线程通知,同时保证了线程并发安全;而 JUC 中还有另外一种 Queue,是并发安全的非阻塞队列,那就是:ConcurrentLinkedQueue 。

适用场景

ConcurrentLinkedQueue 是 java 提供的一个无界非阻塞的 FIFO 队列,具备并发安全特性。适用于多线程共享访问相同的集合,要求多线程主动获取而不是线程阻塞等待通知;并且于队列的大小要求无限制,这常常是使用节点链接的形式来实现。不过,无界的场景也可能会导致内存占用过大。

底层实现(本文基于 jdk15 源码)

从名称我们可以看出来 ConcurrentLinedQueue 是基于节点链接的形式。节点的定义如下:

static final class Node<E> {
    volatile E item;
    volatile Node<E> next;
    Node(E item) {
        ITEM.set(this, item);
    }
    /** Constructs a dead dummy node. */
    Node() {}
    void appendRelaxed(Node<E> next) {
        NEXT.set(this, next);
    }
    boolean casItem(E cmp, E val) {
        return ITEM.compareAndSet(this, cmp, val);
    }
}

内部属性只有 item、next,是一种常见的队列节点结构,使用链接形式。
ConcurrentLinkedQueue 的算法实现基于 Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms( Maged M. Michael , Michael L. Scott),是此论文中非阻塞算法的一种修改实现。

在并发过程中,不使用任何锁如:synchronized、Lock 等,而是纯粹通过 CAS 完成。同时,修改的部分主要有:基于 JVM 的回收环境,让元素能够被自动回收,不会存在 ABA 的循环引用导致回收不了的问题;还有,提供了 remove 操作,支持内部元素删除。

添加元素 add(E)

按照前述节点链接结构,节点通过 next 链接到下一节点,而新节点的 next 总是链接到 null 中。我们可以这样实现,每次添加到队列时,从 head 开始遍历到最后一个节点,并通过 CAS(next,E),这样,我们一定程度上完成了并发的安全性(这里尚未考虑删除情况)。

boolean add(E e){
    E t = head; E next;
    while(t != null){
        if(t.next != null){
            t = t.next;    // 推进到下一节点
        }else if(CAS(t.next,null,e)){    // CAS更新,失败则从头开始
            return true;
        }else{
            t=head;
        }
    }

因为队列是无界的,所以此方法将会一直重试直到添加成功,并永远返回 true。这种实现虽然无锁,不过明显效率低下,所以我们在实现节点链接时,常常会引入 head、tail 指针来辅助推进,避免遍历的情况。同样地,ConcurrentLinkedQueue 使用了这种优化手段。在初始化时,队列就默认初始化了 head指针 和 tail指针,为 dummy 节点。

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

但是,next引用 与 tail指针 是两个不同的属性,而 CAS 在同一个时刻只能更新一个变量,如果想要确保 next引用 与 tail指针 的更新具备原子性,又回到了需要用到锁的境地。ConcurrentLinkedQueue 是如何解决这个问题的?

答案是:延缓更新。在 ConcurrentLinkedQueue 的注释文档中,Doug Lea 提及 LinkedTransferQueue 也同样使用了这种做法,称为:slack threshold。在每一次插入操作之后,tail指针 没有与 next节点 一起做原子性的更新操作。具体我们看下代码,在这里,我们模拟四个并发插入的线程:

/**
 * 队列为无界,所以方法永远不返回 false <br/>
 */
public boolean offer(E e) {
    final Node<E> newNode = new Node<E>(Objects.requireNonNull(e));
    // p、q 前后节点
    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        if (q == null) {
            // 并发 CAS 控制入口
            // p is last node; p 总是最后的节点(即tail)
            if (NEXT.compareAndSet(p, null, newNode)) {
                // 一次跳俩或多节点,允许失败。如果在 if 此处阻塞,将会出现 tail 滞后于 head 的情况 ✌
                // (不一定是只有最后更新的 next 节点的线程才可以更新成功,也就是说更新后的 tail 不一定是最新最准确的)
                // 如果不准确,后续的线程依然可以通过 next 往下推进
                if (p != t) // hop two nodes at a time; failure is OK
                    TAIL.weakCompareAndSet(this, t, newNode);
                return true;
            }
            // Lost CAS race to another thread; re-read next
        }
        else ...
    }
}

① 当线程一添加元素时,head=tail=new Node(),将 p(=>pointer:指针) 赋值为 tail,将 q 赋值为 p 的下一个节点,即 p=tail.next=null,所以进入后线程一能直接 CAS 插入节点,此时 p=t=tail,所以第一个更新的线程满足 p!=t,不会更新 tail指针。

② 并发的另外三个线程因为 CAS失败,重新进入循环。此时 q = tail.next != null,进入第二个分支判断 p==q,这是插入时针对并发删除的分支处理。在没有删除操作时,p 总是不等于 q,因为 p = tail; q = p.next = tail.next

else if (p == q)
    // 此处是与删除操作的多线程处理
    // 前后节点相等,说明该节点的已经被执行过移除操作
    // 节点移除后,如果此时 tail 未变更(可能阻塞了),也组织不了它被移除的命运。
    // 此时,我们要做的是将指针跳到 head,一边它能够从头遍历。否则更新 tail 更好。
    // 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;

目前在单纯的并发插入操作中,q 总是 p 的下一个节点(next),可能为新节点&

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值