深入理解ConcurrentLinkedQueue源码

news2024/11/16 15:40:49

1. 概述

在我们的日常开发中,经常会使用队列这种数据结构,需要它的队尾进、队头出的特点。于是,Doug Lea大师设计了一个线程安全的队列ConcurrentLinkedQueue,它是采用链表的形式构成的。我们接下来尝试通过代码去了解其中的设计思想。

2. 成员变量

    private static class Node<E> {
        volatile E item;  // 元素值
        volatile Node<E> next;  // 指向下一个节点
    }

ConcurrentLinkedQueue底层采用的是链表的数据结构,因此通过静态内部类定义了链表节点Node,包含两个基本的变量。

    // 头指针
    private transient volatile Node<E> head;
    // 尾指针
    private transient volatile Node<E> tail;

成员变量也比较简单,包括头指针和尾指针。

3. 构造方法

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

常用的构造方法是这个无参的构造方法,创建了一个空节点,并让头指针和尾指针都指向这个节点。具体状态如下图所示:
在这里插入图片描述

4. offer方法

    public boolean offer(E e) {
        checkNotNull(e);  // 插入的元素必须非null
        final Node<E> newNode = new Node<E>(e);  // 创建新的节点

        for (Node<E> t = tail, p = t;;) { // 尾指针指向赋值为p
            Node<E> q = p.next;  // q指向p的下一个节点
            if (q == null) {  // 如果p是真正的尾节点
                if (p.casNext(null, newNode)) {  // 通过CAS方式设置尾节点为新的节点
                    if (p != t) // 如果尾指针指向的不是真的尾节点
                        casTail(t, newNode);  // CAS更新尾指针,允许失败
                    return true;
                }
            }
            else if (p == q) // 哨兵,有元素从队头出队了
                p = (t != (t = tail)) ? t : head;
            else // 定位新的队尾节点
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

初次看上面这份offer的代码,可能会看得云里雾里的,因为这份代码考虑了很多多线程不安全的情况,所以就会比较复杂。

我们先从单线程角度来看这份代码。首先,尾指针tail指向的节点赋值给节点p,p的下一个节点被赋值给节点q。然后判断q为null的话,就意味着p就是链表中的最后一个节点。所以就尝试用CAS方法将p的下一个节点设置为newNode,如果设置成功,接着尝试用CAS方法设置newNode为尾节点。这和我们常见的尾插法是一样的,但是因为casTail方法可能失败,就有可能出现下面这种情况:
在这里插入图片描述

尾指针不一定在每一时刻都指向真正的尾节点,可能存在延迟更新的情况。 因此代码中,并不会直接对Tail指向的节点插入newNode,而是通过p去寻找真正的尾节点,再用CAS的方式插入newNode。

我们再来看这行代码

p = (p != t && t != (t = tail)) ? t : q;

如果是单线程情况下,那么t肯定是等于tail的,所以三元表达式的值一直都是p=q,也即让p不断向后寻找,直到找到真的尾节点。

但是在多线程情况下,t != (t = tail)其实不是一个原子操作,可能会出现这样的情况:
在这里插入图片描述

对于等式的左边,线程A先读取了变量t。接着线程B立刻修改了尾节点,这就会造成线程A在读取(t=tail)时发现两者不一样,就会得到p=t的结果。这样做的 意义在于此时(t=tail)是最新的尾节点,所以让p指向t可以更快地找到尾节点进行插入操作

而对于这段代码

else if (p == q):
    p = (t != (t = tail)) ? t : head;

这主要是涉及到哨兵节点,当发现有线程在队头出队列后,就会出现p=q的情况,就需要从头指针重新开始搜索头节点,即p=head。

5. poll方法

    public E poll() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {  // 头指针节点复制给p,h
                E item = p.item;  // p节点的元素值

                // 如果不为null,说明是真的头节点,则CAS尝试设置元素值为null
                if (item != null && p.casItem(item, null)) {
                    // 如果p节点和最初的h节点不一样,说明head指针指向的不是真的头节点
                    if (p != h) 
                        // 更新头节点
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                // 如果没有下一个节点了,无法删除了
                else if ((q = p.next) == null) {
                    updateHead(h, p);  // 同样更新头节点
                    return null;
                }
                else if (p == q)   // 哨兵节点情况
                    continue restartFromHead;  // 一切从头来过
                else  // 寻找真的头节点,p节点不断往后寻找
                    p = q;
            }
        }
    }

    final void updateHead(Node<E> h, Node<E> p) {
        if (h != p && casHead(h, p))  // CAS设置新的头节点
            h.lazySetNext(h);
    }

    // CAS设置下一个节点
    void lazySetNext(Node<E> val) {
        UNSAFE.putOrderedObject(this, nextOffset, val);
    }

poll方法实际上和offer方法的思想是一样的,都是使用了延迟更新的方式。当单线程的情况下,要删除队头元素,就是要找到头指针指向的节点,进行删除就行了。但是在多线程情况下,头指针指向的节点不一定是真的头节点,可能这个节点之前已经被删除过了。如初始状态:
在这里插入图片描述

因此,代码中首先将头指针指向的节点赋值给p和h。如果p中的元素值不为null,说明是真的头节点,尝试用CAS设置元素值为null,即完成出队操作。如果p中的元素值为null,那就说明这个节点之前就已经被删除过了,就需要通过p= q = p.next不断往后寻找,找到真正的头节点。比如下图就是head节点实际指向了已经被删除过的节点:
在这里插入图片描述

因此就通过p节点去寻找到真正的头节点:
在这里插入图片描述

然后我们通过下面的代码去更新新的头节点:

final void updateHead(Node<E> h, Node<E> p) {
        if (h != p && casHead(h, p))  // CAS设置新的头节点
            h.lazySetNext(h);
}

在这里插入图片描述

在代码中,我们会看到h.lazySetNext(h)这一行,其实就是将已经删除过的节点自己指向自己。这么做的原因就是会提醒offer和poll线程,有元素出队了,需要重新计算,就会出现之前说的p=q的情况,因为q=p.next。这样就是我们哨兵节点的作用

6. 总结

ConcurrentLinkedQueue的设计方式采用的是延迟更新头指针和尾指针。通常我们为了保证同步,在入队和出队的时候需要加入synchronized或者锁,但是这种方式是消耗很大的。而Doug Lea大师使用CAS的方式轻松化解线程不安全问题。其实他也可以通过自旋的方式来做到实时更新头指针和尾指针,但是这会带来一定的消耗。延迟更新的设计方式则可以大大减少CAS的次数,提升效率。

参考文章:并发容器之ConcurrentLinkedQueue

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/130471.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

S7-200SMART通过表格指令实现模拟量信号滑动平均值滤波的具体方法

S7-200SMART通过表格指令实现模拟量信号滑动平均值滤波的具体方法 当现场的模拟量信号波动太大,而通过硬件的方式尚无法实现平稳的信号输入时,可采用软件上的滤波进行信号处理, 本次和大家分享的即通过取多个信号值的平均值的方式实现模拟量滤波的具体方法示例,仅供大家参考…

DataNode节点下线速度优化

目录 一、节点掉线或退役 1.1区分节点掉线和节点退役的区别 1.2 如何处理节点掉线出现的各种风暴 1.2.1 Datanode的block复制 1.2.2 控制节点掉线RPC风暴的参数 二、如何快速节点下线 一、节点掉线或退役 背景&#xff1a;5台数据节点&#xff0c;存储40T数据 block数112…

高等数学(上) —— 一元积分学

文章目录Ch4.不定积分原函数F(x)F(x)F(x)原函数存在定理不定积分∫f(x)dx\int f(x)dx∫f(x)dx不定积分公式不定积分 ⇦⇨ 变上限积分&#xff1a;∫f(x)dx∫0xf(t)dt\int f(x){\rm d}x\int_0^xf(t){\rm d}t∫f(x)dx∫0x​f(t)dtCh5.定积分1.定积分定义定积分的几何意义2.定积分…

ESP32的python开发环境搭建:Thonny+MicroPython

1 Thonny安装 Thonny —— 一个面向初学者的 Python IDE。Thonny良好的支持Microbit、ESP32和树莓派等的开发. 安装下载地址&#xff1a; https://thonny.org/ 2 Micropython安装 MicroPython 是 Python 3 语言的精简实现 &#xff0c;包括Python标准库的一小部分&#xff0…

Vector - VT System - 板卡_VT8006/VT8012

由于最近不幸变为了小*人&#xff0c;因此断更了一周&#xff0c;今天稍有好转&#xff0c;就新加一块大家应该会比较感兴趣的VT板卡硬件介绍吧&#xff0c;也预示着新的开始&#xff0c;马上也要到了元旦&#xff0c;新的一年即将开始&#xff0c;提前在这里祝福大家在新的一年…

JavaPub面试宝典【第22版】

JavaPub面试宝典【第22版】 直接上干货&#xff0c;几百篇原创笔记都在这。 文章列表 &#x1f4da;最少必要面试题 Java基础Java并发入门Java容器JavaWebJVMMySQLMyBatisSpringSpringBootRedisElasticSearchKafkaZookeeperDocker缓存 &#x1f4d6;知识点总结 下面是原创…

linux 下命令

linux 下命令 Linux 是一套免费使用和自 由传播的类 Unix 操作系统&#xff0c; 是一个基于 POSIX 和 UNIX 的多用户、 多任务、 支持多线程和多 CPU 的操作系统。 它能运行主要的 UNIX 工具软件、 应用程序和网络协议。 它支持 32 位和 64 位硬件。 Linux 继承了 Unix 以网络为…

uniCloud云开发----4、uniCloud云开发进阶使用方法

uniCloud云开发进阶使用方法前言1、云对象的importObject的创建和使用(1&#xff09;创建云对象&#xff08;2&#xff09;编辑云对象&#xff08;3&#xff09;在.vue文件中调用云对象&#xff08;4&#xff09;在.vue文件中调用方法2、客户端直接连接数据库(1)直接在客户端引…

设计模式-牛刀小试02

前言 本文为datawhale2022年12月组队学习《大话设计模式》最后一次打卡任务&#xff0c;本次完成homework2。 【教程地址】https://github.com/datawhalechina/sweetalk-design-pattern 一、任务描述 1.1 背景 小李已经是一个工作一年的初级工程师了&#xff0c;他所在的公…

sqlite wal 分析

动手点关注干货不迷路sqlite 提供了一种 redo log 型事务实现&#xff0c;支持读写的并发&#xff0c;见 write-ahead log&#xff08;https://sqlite.org/wal.html&#xff09;。本文将介绍 wal 原理&#xff0c;并源码剖析 checkpoint 过程&#xff0c;同时讨论下 wal 使用中…

知行之桥EDI系统如何通过ZIP端口压缩文件?

在EDI项目当中&#xff0c;对于IT技术不够成熟或设备不够完善的用户来说&#xff0c;EXCEL方案是较为适中的选择。收到合作伙伴发来的850订单之后&#xff0c;将订单数据转换为EXCEL&#xff0c;再将EXCEL发送至用户指定的邮箱。若每条订单单独发送至邮箱&#xff0c;当订单量大…

#榜样的力量#《新冠战“疫”——2022中国数据智能产业最具社会责任感企业》榜正式发布...

‍数据猿出品数据猿此次推出的#榜样的力量#《新冠战“疫”——2022中国数据智能产业最具社会责任感企业》榜单/奖项为公益主题策划活动。旨在为正能量助威&#xff0c;为中国数据智能产业中具有社会责任感的典型性企业发声。数据智能产业创新服务媒体——聚焦数智 改变商业新冠…

66、【链表】leetcode——142. 环形链表 II(C++、Python版本)

题目描述 方法一&#xff1a;哈希表 C版本 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode(int x) : val(x), next(NULL) {}* };*/ class Solution { public:ListNode *detectCycle(ListNode *head) {unord…

年终小结:苟住

大家好&#xff0c;我是一哥&#xff0c;新的一年又要到来了&#xff0c;一个词总结下今年的状态 01 工作 2022真的比较难&#xff0c;作为互联网技能从业人员&#xff0c;相信大家一定经历了很多。这一年虽然我没有被毕业&#xff0c;但也经历了公司的业务调整&#xff0c;曾…

C51 --串口通信

3.2.1 串口的连接方式 RXD&#xff1a;数据输入引脚&#xff0c;接受数据&#xff1b;STC89系列对应P3.0口&#xff0c;上官一号有单独引出 TXD&#xff1a; 数据发送引脚&#xff0c;数据发送&#xff1b;STC89系列对应P3.0口&#xff0c;上官一号有单独引出 接线方式&…

2022 IoTDB Summit:IoTDB PMC 田原《大规模并行处理与边缘计算在 Apache IoTDB 中的实践》...

12 月 3 日、4日&#xff0c;2022 Apache IoTDB 物联网生态大会在线上圆满落幕。大会上发布 Apache IoTDB 的分布式 1.0 版本&#xff0c;并分享 Apache IoTDB 实现的数据管理技术与物联网场景实践案例&#xff0c;深入探讨了 Apache IoTDB 与物联网企业如何共建活跃生态&#…

带头双向循环链表的增删改查

目录&#x1f921;前言&#x1f60a;带头双向循环链表&#x1f44d;链表实现&#x1f620;插入-头插&#xff0c;尾插&#xff0c;随机插✅删除-头删&#xff0c;尾删&#xff0c;随机删&#x1f602;链表搜索和链表修改&#x1f4a1;总结&#x1f921;前言 本篇博客主要是介绍…

Qt编写雷达模拟仿真工具(模拟点/歼击机/航母/发射导弹/爆炸效果/激光雷达等)

一、简单介绍 雷达模拟仿真工具&#xff0c;主要通过模拟点模拟相关物体&#xff0c;方位、航向角、距离、速度&#xff0c;并且显示相关详情信息可建立跟踪线建立与模拟点联系。可自定义更换模拟点背景达到更加逼真效果&#xff0c;如歼击机&#xff0c;航母发射导弹效果&…

单例模式,适配器模式,迭代器模式,工厂模式(C++实现)

设计模式就相当于编程中的“孙子兵法”&#xff0c;是经过很久的时间以及各路大神总结出来的多种实用&#xff0c;高效的业务设计中的套路; 单例模式 一个类只能创建一个对象的情况下的一种设计模式&#xff08;eg&#xff1a;服务器只有一个&#xff09;&#xff0c;即单例模…

【论文阅读】《知识图谱研究综述》;Knowledge Graph:概念及主要应用,主要特征、构建的主要技术、未来研究方向。

目录 1. 文章来源2. 简介3. 什么是知识图谱4. Knowledge Graph 主要特征5. Knowledge Graph 构建的主要技术6. Knowledge Graph 未来研究方向1. 文章来源 该paper来自于知网,请尊重原创,这里仅作为学习笔记~ 2. 简介 知识图谱将知识库以一种图谱的形式展现出来,使知识具有…