导读:本期聚焦于小伙伴创作的《ConcurrentLinkedDeque如何处理首尾并发存取?深入解析双向队列逻辑》,敬请观看详情。为什么多数线程在抢占头尾节点时不会相互阻塞?ConcurrentLinkedDeque基于无锁链表与CAS操作,将头尾指针拆分为独立的可变状态。头插与尾插各自依赖head、tail的松弛更新策略,使多线程首尾同时存取时仍能保持线性一致。其节点删除采用逻辑移除再物理解链,避免读写竞争导致的数据断裂。理解这些机制能帮你在高并发日志收集、任务双端调度中正确评估其性能边界。

ConcurrentLinkedDeque是JDK中提供的线程安全双向并发队列,它采用非阻塞算法实现,允许从队列头部和尾部同时进行插入、删除与遍历操作。与同步容器不同,它不依赖锁,而是借助CAS(Compare And Swap)和细粒度的内存可见性控制来保证线程安全。在处理首尾变量存取时,其内部逻辑尤其精巧,既避免了伪共享带来的性能损耗,也降低了线程争用概率。

ConcurrentLinkedDeque如何处理首尾并发存取?深入解析双向队列逻辑

一、核心数据结构与头尾指针设计

ConcurrentLinkedDeque内部维护一个双向链表,每个节点为Node对象,包含item(数据)、prev(前驱)和next(后继)指针。与单向队列不同,它显式持有head和tail两个引用,但在大部分时刻这两个引用是“松弛”的,即它们不一定指向真正的首个或末个存活节点,而只保证通过它们能遍历到有效端点。

这种设计的核心原因在于:如果每次存取都强制将head或tail更新到准确的边界节点,就会在多线程场景下引发频繁的CAS竞争。通过允许指针滞后,线程在本地缓存中操作节点,仅在必要时才执行跨线程的指针修正,从而显著提升吞吐量。下面的代码展示了Node的基础定义:

static final class Node<E> {
    volatile E item;
    volatile Node<E> prev;
    volatile Node<E> next;

    Node(E item) {
        // 使用UNSAFE.putObject保证可见性
        this.item = item;
    }
}

从内存布局看,head和tail被声明为volatile,确保多线程下读取到的引用是最新提交的值。但正如前文所说,它们只是“逻辑锚点”。例如,当从头部出队时,若head指向的节点已被移除,算法会从head出发沿next向后寻找第一个item非空的节点,并顺便将head松弛地推进到该位置。

这种松弛策略的代价是遍历可能多走几步,但收益是减少了写冲突。在尾部入队时同理:tail可能停在倒数第二个节点,新节点插入后,只有当下次插入发现tail.next不为空时才更新tail。该权衡在高并发首尾混合访问时极为关键。

二、头部存取的底层逻辑

头部操作主要包括addFirst(头插)和pollFirst(头取)。头插时,新节点被链接到当前head之前,并通过CAS将head指向新节点。若CAS失败,说明其他线程已修改head,此时线程会重新读取head并重试,这种自旋不放弃CPU但也不会阻塞。

具体来看,头插逻辑会先定位“真实头”:从head开始向后跳过已被逻辑删除(item为null)的节点。找到后,将新节点的next指向该节点,该节点的prev指向新节点,最后CAS更新head。下面简化代码说明了这一流程:

private void linkFirst(E e) {
    Node<E> newNode = new Node<E>(e);
    restart:
    for (;;) {
        Node<E> h = head;
        Node<E> first = h.next; // 可能需跳过期节点
        // 寻找第一个有效节点
        while (first != null && first.item == null) {
            first = first.next;
        }
        newNode.next = first;
        if (first != null) first.prev = newNode;
        if (casHead(h, newNode)) {
            break;
        }
    }
}

在pollFirst中,线程不会立即将节点从链表物理摘除,而是先将item通过CAS置为null(逻辑删除),再在后续遍历中跳过它,并由其他操作顺势调整链接。这避免了在读取线程正遍历该节点时修改指针造成混乱。

这种“先逻辑删、后物理松绑”的方式,使头部读取(peekFirst)即使遇到并发删除也能返回一致性视图:要么看到旧值,要么看到新头,不会出现中间态。对于需要严格顺序的消费场景,该特性非常重要。

三、尾部存取的底层逻辑

尾部操作addLast和pollLast与头部对称,但方向朝前。尾插时,新节点链接到tail之后,并视情况更新tail。由于tail的松弛性,插入线程可能连续多次不更新tail,直到某次发现tail.next非空才顺带推进。

以下代码展示了尾插的核心循环:

private void linkLast(E e) {
    Node<E> newNode = new Node<E>(e);
    for (;;) {
        Node<E> t = tail;
        Node<E> last = t.prev; // 类似头部,向前找真实尾
        while (last != null && last.item == null) {
            last = last.prev;
        }
        newNode.prev = last;
        if (last != null) last.next = newNode;
        if (casTail(t, newNode)) {
            break;
        }
    }
}

尾取时,算法从tail向前寻找最后一个item非空的节点,逻辑删除其item,并可能将tail回退到更靠近有效节点的位置。注意,由于双向结构,尾取和头插可能在同一时刻操作相邻节点,但因为各自只修改自己一侧的链接且依赖CAS,不会相互覆盖。

在极高并发下,首尾同时存取会形成一种“对流”效应:头部线程向后清理,尾部线程向前清理,中间节点逐渐被逻辑删除。这种结构虽然暂时冗余,但保证了无锁环境下的安全,也解释了为何size()方法需要遍历全表且结果不精确。

四、首尾并发存取的一致性保障

ConcurrentLinkedDeque遵循弱一致性迭代器规范,且不抛出ConcurrentModificationException。当多线程分别从头和尾存取时, Happens-Before关系由volatile读写与CAS的硬件屏障共同建立。一次成功的CAS同时具备原子性与可见性,使得头尾变量的修改对其它线程及时可读。

我们可通过一个简单测试观察首尾并发行为:

ConcurrentLinkedDeque<Integer> deque = new ConcurrentLinkedDeque<>();
// 线程A持续头插
new Thread(() -> {
    for (int i = 0; i < 1000; i++) deque.addFirst(i);
}).start();
// 线程B持续尾插
new Thread(() -> {
    for (int i = 0; i < 1000; i++) deque.addLast(i);
}).start();
// 主线程混合取
while (!deque.isEmpty()) {
    deque.pollFirst();
    deque.pollLast();
}

上述代码在运行期不会出现数据丢失,因为头尾操作各自维护边界指针,插入和删除均基于CAS重试。即使某线程在更新head瞬间被挂起,其他线程也能通过遍历找到正确边界,不会误判队列为空。

不过需要注意,该队列不支持阻塞等待,若业务要求“队列空时等待”,需配合外部同步或使用LinkedBlockingDeque。此外,由于松弛指针的存在,监控时不能依赖head或tail的直接位置来判断元素数量,而应接受近似性。

五、实践中的注意事项

在使用ConcurrentLinkedDeque处理首尾变量存取时,首先应明确场景是否真正需要双端并发。若只有单端生产单端消费,其性能可能不如ArrayDeque加外部锁直观,但在多生产者多消费者双端模式(如工作窃取池的任务队列)中优势明显。

其次,避免在迭代中假设快照一致。例如以下代码可能遗漏或重复元素:

for (Integer x : deque) {
    // 迭代期间其他线程头尾修改
    System.out.println(x);
}

最后,由于节点在逻辑删除后不会立即回收,长时间极高吞吐且元素体积大的情况下,应注意堆内存压力。可通过定期调用一次poll类操作触发链接整理,或评估是否改用有界结构。理解其头尾存取逻辑,才能在高并发系统中扬长避短。

ConcurrentLinkedDeque非阻塞算法双向队列修改时间:2026-08-02 13:33:16

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。