深入浅出ConcurrentLinkedQueue(一)

ConcurrentLinkedQueue 是 java.util.concurrent(JUC) 包下的一个线程安全的队列实现。基于非阻塞算法(Michael-Scott非阻塞算法的一种变体),这意味着 ConcurrentLinkedQueue 不再使用传统的锁机制来保护数据安全,而是依靠底层原子的操作(如 CAS)来实现。
Michael-Scott 由 Maged M. Michael 和 Michael L. Scott 在 1996 年提出,在这种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起。
接下来看一下 ConcurrentLinkedQueue 的源码实现。

节点类Node

先从它的节点类 Node 看起,好明白 ConcurrentLinkedQueue 的底层数据结构。Node 类的源码如下:

java 复制代码
private static class Node<E> {
        volatile E item;
        volatile Node<E> next;
		.......
}

Node 节点包含了两个字段:

  • 一个是数据域 item
  • 另一个是 next 指针,用于指向下一个节点从而构成链式队列。
    两个字段都是用 volatile 修饰的,以保证内存的可见性。
    另外,ConcurrentLinkedQueue 还有这样两个成员变量:
java 复制代码
private transient volatile Node<E> head;
private transient volatile Node<E> tail;


说明 ConcurrentLinkedQueue 通过持有头尾两个引用来进行队列管理。当我们调用无参构造方法时,其源码如下:

java 复制代码
public ConcurrentLinkedQueue() {
    head = tail = new Node<E>(null);
}

head 和 tail 会指向同一个节点,此时 ConcurrentLinkedQueue 的状态如下图所示:
ConcurrentLinkedQueue初始化状态

head 和 tail 指向同一个节点 Node0,该节点的 item 字段为 null,next 字段也为 null。
在队列进行出队入队的时候,免不了要对节点进行操作,在多线程环境下就很容易出现线程安全问题。ConcurrentLinkedQueue 选择使用 CAS 来保证线程安全:

java 复制代码
//更改Node中的数据域item
boolean casItem(E cmp, E val) {
    return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
//更改Node中的指针域next
void lazySetNext(Node<E> val) {
    UNSAFE.putOrderedObject(this, nextOffset, val);
}
//更改Node中的指针域next
boolean casNext(Node<E> cmp, Node<E> val) {
    return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}

可以看出,这些方法实际上调用的是 UNSAFE 的方法:

sun.misc.Unsafe 是 Java 内部的一个类,它提供了一组可以直接访问底层资源和操作内存的方法。这个类的功能非常强大,因为它允许程序绕过 Java 的访问控制和安全检查,直接执行底层操作。
Unsafe 允许分配、释放和访问本机内存,就像使用 C 语言中的 malloc 和 free 一样。

offer方法

ConcurrentLinkedQueue 是一种先进先出(FIFO,First-In-First-Out)的队列,offer 方法用于在队列尾部插入一个元素。如果成功添加元素,则返回 true。下面是这个方法的一般定义:

java 复制代码
public boolean offer(E e)

来看这么一段代码:

java 复制代码
ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
queue.offer(1);
queue.offer(2);

创建一个 ConcurrentLinkedQueue 对象 queue,先 offer 1,再 offer 2。其中 offer 的源码如下:

java 复制代码
public boolean offer(E e) {
    checkNotNull(e);
    final Node<E> newNode = new Node<E>(e);

    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        if (q == null) {
            // p is last node
            if (p.casNext(null, newNode)) {
                // Successful CAS is the linearization point
                // for e to become an element of this queue,
                // and for newNode to become "live".
                if (p != t) // hop two nodes at a time
                    casTail(t, newNode);  // Failure is OK.
                return true;
            }
            // Lost CAS race to another thread; re-read next
        }
        else if (p == q)
            // 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;
        else
            // Check for tail updates after two hops.
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

1、参数检查:checkNotNull(e) 确保传递的元素不是 null。
2、新节点创建:final Node newNode = new Node(e) 创建一个新的节点来保存要添加的元素。
3、尾部节点循环:该循环用于找到队列的尾部节点,并将新节点安全地链接到尾部。

  • a. 读取下一个节点:Node q = p.next 读取当前节点的下一个节点。
  • b. 尾部节点检查:如果 q 是 null,这意味着当前节点 p 是尾部节点。
  • c. CAS操作添加新节点:p.casNext(null, newNode) 使用 CAS 操作将新节点链接到当前的尾部节点。如果成功,则更新尾部引用,并返回 true。
  • d. 双跳尾部更新:casTail(t, newNode) 有时尝试更新尾部引用,使其指向新的尾部节点。这有助于其他线程更快地找到尾部。
  • e. 掉出列表检查:如果 p == q,这意味着当前线程从列表上掉了下来。此时,代码尝试跳转到头部或新的尾部。
  • f. 进一步检查:否则,代码进行进一步的检查并更新 p 的值,可能是当前的尾部或下一个节点。
    我把代码注释去掉,并标上行号。
java 复制代码
public boolean offer(E e) {
1.    checkNotNull(e);
2.    final Node<E> newNode = new Node<E>(e);
3.    for (Node<E> t = tail, p = t;;) {
4.        Node<E> q = p.next;
5.        if (q == null) {
6.            // p is last node
7.            if (p.casNext(null, newNode)) {
8.                if (p != t)
9.                    casTail(t, newNode);
10.                return true;
            }
        }
11.        else if (p == q)
12.            p = (t != (t = tail)) ? t : head;
           else
13.            p = (p != t && t != (t = tail)) ? t : q;
    }
}

单线程执行角度分析

单线程的角度分析 offer 1 的过程。
第 1 行代码检查元素 e 是否为 null,为 null 就直接抛出空指针异常。
第 2 行代码将 e 包装成一个 Node 对象。
第 3 行为 for 循环,只有初始化条件没有循环结束条件,这很符合 CAS 的"套路",在循环体内,如果 CAS 操作成功会直接 return 返回,如果 CAS 操作失败就在 for 循环中不断重试直至成功。这里实例变量 t 被初始化为 tail,p 被初始化为 t 即 tail。
p 被认为是队列真正的尾节点,tail 不一定是真正的尾节点,因为在 ConcurrentLinkedQueue 中 tail 延迟更新的
代码走到第 3 行的时候,t 和 p 分别指向初始化时创建的 item(null),next 字段也为 null,即 Node0。
第 4 行变量 q 被赋值为 null。
第 5 行 if 判断结果为 true。
第 7 行使用 casNext 将插入的 Node 设置为当前队列尾节点 p 的 next 节点,如果 CAS 操作失败,此次循环结束,下次循环进行重试。
CAS 操作成功走到第 8 行,此时 p==t,if 判断为 false,直接 return true 返回。如果成功插入 1 的话,此时 ConcurrentLinkedQueue 的状态如下图所示:
offer 1后队列的状态

此时队列的尾节点应该是 Node1,而 tail 指向的节点依然是 Node0,因此可以说明 tail 是延迟更新的。
那么继续看 offer 2,很显然此时第 4 行 q 指向的节点不为 null 了,而是指向 Node1,第 5 行 if 判断为 false,第 11 行 if 判断为 false,代码会走到第 13 行。
好了,第 13 行代码是找出队列真正的尾节点

java 复制代码
p = (p != t && t != (t = tail)) ? t : q;

这段代码在单线程环境 执行时,由于 p==t,此时 p 会被赋值为 q,而 q 等于Node q = p.next,即 Node1。
在第一次循环中,p 指向了队列真正的尾节点 Node1,那么在下一次循环中,第 4 行 q 指向的节点为 null,那么第 5 行 if 判断则为 true,第 7 行依然通过 casNext 设置 p 节点的 next 为当前新增的 Node,接下来走到第 8 行,这个时候 p!=t,第 8 行 if 判断为 true,会通过casTail(t, newNode)将当前节点 Node 设置为队列的尾节点,此时的队列的状态示意图如下图所示:
队列offer 2后的状态

tail 指向的节点由 Node0 变为 Node2 ,这里的 casTail 是不需要重试的,原因是,offer 主要是通过 p 的 next 节点 q(Node q = p.next)决定后面的逻辑走向,casTail 失败时状态示意图如下:
队列进行入队操作后casTail失败后的状态图

如果 casTail 更新 tail 失败,即 tail 还是指向 Node0 节点,无非就是多循环几次,通过第 13 行代码定位到尾节点
通过单线程执行角度的分析,可以了解到 offer 的执行逻辑为:

  1. 如果 tail 节点的下一个节点(next 字段)为 null 的话,说明 tail 节点即为队列真正的尾节点,因此可以通过 casNext 插入当前待插入的节点,但此时 tail 并未变化
  2. 如果 tail 节点的下一个节点(next 字段)不为 null 的话,说明 tail 节点不是队列的真正尾节点。通过 q(Node q = p.next) 往前找到尾节点,然后通过 casNext 插入当前待插入的节点,并通过 casTail 方式更新 tail
    在单线程环境下,p = (p != t && t != (t = tail)) ? t : q;这行代码永远不会将 p 赋值为 t,尝试在多线程的环境下继续分析。

多线程执行角度分析

多线程环境 下,p = (p != t && t != (t = tail)) ? t : q; 这行代码就有点东西了。
由于 t != (t = tail) 这个操作并非一个原子操作,所以就有这样一种情况:
线程A和线程B有可能的执行时序

假设线程 A 此时读取了变量 t,线程 B 刚好在这个时候 offer 一个 Node,此时会修改 tail,那么线程 A 再次执行 t=tail 时,t 会指向另外一个节点,很显然线程 A 前后两次读取的变量 t 指向的节点不同,即t != (t = tail)为 true,并且由于 t 节点的变化,p != t也为 true,此时该行代码的执行结果是:p 和 t 都指向了同一个节点,并且 t 也是队列真正的尾节点。也就是说,现在已经定位到队列真正的尾节点,可以执行 offer 操作了。
到此为止,还剩下第 11 行的代码没有分析,大家应该可以猜到这种情况:一部分线程 offer,一部分线程 poll
当if (p == q)为 true 时,说明 p 节点的 next 也指向它自己,这种节点称之为哨兵节点这种节点在队列中存在的价值不大,一般表示要删除的节点或者空节点

以上是第一部分内容。

相关推荐
寻寻觅觅☆6 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
YJlio6 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
偷吃的耗子7 小时前
【CNN算法理解】:三、AlexNet 训练模块(附代码)
深度学习·算法·cnn
l1t7 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
赶路人儿7 小时前
Jsoniter(java版本)使用介绍
java·开发语言
化学在逃硬闯CS7 小时前
Leetcode1382. 将二叉搜索树变平衡
数据结构·算法
ceclar1238 小时前
C++使用format
开发语言·c++·算法
山塘小鱼儿8 小时前
本地Ollama+Agent+LangGraph+LangSmith运行
python·langchain·ollama·langgraph·langsimth
码说AI8 小时前
python快速绘制走势图对比曲线
开发语言·python