concurrentlinkedqueue中不存在pointee变量,其通过volatile node next和object item字段实现无锁队列;空节点指item==null且已被逻辑删除的节点,跳过时依赖item==null与next!=null双重检查,而非额外标记字段。

ConcurrentLinkedQueue 里没有 Pointee 变量——这是个常见误解,源于混淆了不同语言或教学模型中的术语。Java 标准库的 ConcurrentLinkedQueue 使用的是基于 Node 节点的 CAS 链表结构,其核心是 volatile Node next 和 Object item 字段,不存在名为 Pointee 的字段或概念。
如果你在某篇博客、PPT 或 Rust/Go 的类比实现中看到 Pointee,那大概率是作者为讲解方便自定义的示意性字段(比如指向“逻辑上应被跳过的节点”),不是 JDK 源码的一部分。
ConcurrentLinkedQueue 怎么识别和跳过“空节点”
- “空节点”在源码中特指
item == null且已被出队(casItem(item, null)成功)的节点,它们仍留在链表中,但不再代表有效元素 - 入队(
offer)和出队(poll)都必须主动跳过这类节点,否则会破坏线性一致性 - 跳过逻辑不靠额外标记字段,而是通过双重检查:
item == null+next != null(说明它已被逻辑删除)
// 简化自 JDK 8 的 poll() 片段
Node p = head;
for (;;) {
Node h = p, s = p.next;
if (h == p) { // 检查是否被其他线程修改过 head
if (s == null) return null; // 队列空
else if (s.item != null) // 找到首个非空节点 → 返回
return s.item;
else // s.item == null → 是空节点,跳过它
p = s;
}
}
为什么不用额外字段(如 Pointee)标记“跳转目标”
增加字段意味着更多内存占用和 CAS 开销(每次修改需更新更多字节)
空节点的“跳转目标”本身是动态的:可能被后续线程再次推进,也可能因并发入队而失效
实际采用的“前驱节点尝试 CAS 更新 next 指向后继”策略(即 helpDelete)已足够轻量,无需预存跳转地址
-
ConcurrentLinkedQueue的无锁设计依赖“状态+动作”耦合:-
item == null表示该节点已被逻辑删除 -
next != null表示它尚未被完全从链表中“绕过” - 线程在遍历时顺次推进,自然完成清理(lazy cleanup)
-
容易踩的坑:误把 next == null 当作队尾,忽略空节点链
错误认知:
tail.next == null就代表队列结束实际情况:尾部可能堆积多个
item == null的节点(尤其高并发出队后),tail并不总指向真实尾节点正确做法:必须从
head或当前tail出发,沿next向后扫描,直到找到item != null的节点,或遇到next == null且item == null的真正尾节点调试时可用
toString()或断点观察node.item和node.next的实时值,别只看引用链形状
真正的难点不在“怎么跳”,而在“什么时候该跳、跳到哪一步停住”——这由多个线程对同一节点的 CAS 尝试顺序决定,无法靠单次读取字段预测。这也是为什么所有正确实现都强调“循环重试 + volatile 读 + CAS 写”的组合,而不是依赖某个静态标记。











