ConcurrentLinkedQueue 的无锁特性依赖 CAS 原子操作、volatile 可见性与链表松弛更新协同实现;其核心是通过 volatile head/tail 和 Node 字段,配合非阻塞的 offer/poll 逻辑与 succ() 跳过已删除节点,保障高并发下的线程安全。

ConcurrentLinkedQueue 的无锁特性不是靠“不加锁”来实现的,而是靠一套精心设计的 CAS 原子操作 + volatile 可见性 + 链表结构松弛更新机制协同完成。分析它的源码,关键不在“有没有锁”,而在于看清楚它如何用非阻塞方式协调多个线程对 head/tail 和节点 next/item 的并发修改。
核心结构:两个 volatile 节点引用 + 单向链表 Node
-
head和tail都是volatile Node<E>类型,保证多线程读写可见,但它们不总是实时指向逻辑首尾。 - 每个
Node包含volatile E item和volatile Node<E> next,所有字段更新都通过UNSAFE.compareAndSwapXXX(CAS)完成。 - 构造时初始化为同一个哨兵节点(
item == null),形成初始空队列。
入队(offer)的关键逻辑:找尾、CAS 插入、懒更新 tail
- 从
tail开始尝试定位最后一个next == null的节点(即物理尾部)。 - 若
p.next == null,说明p是当前尾节点 → 尝试p.casNext(null, newNode)。- 成功:新节点插入成功;若
p != tail,再尝试casTail(tail, newNode)更新 tail(非必须,失败也无妨)。 - 失败:说明其他线程已抢先修改,循环重试。
- 成功:新节点插入成功;若
- 若
p.next != null或遇到p == p.next(自循环节点,表示该节点已被出队逻辑标记为“逻辑删除”),则沿链表推进或跳转到tail重新定位。
出队(poll)的关键逻辑:跳过已删除节点、CAS 清空 item
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
- 从
head开始遍历,找第一个item != null的节点。 - 对该节点执行
p.casItem(item, null):- 成功:逻辑上移除该元素,返回原值;随后判断是否需
updateHead(跳过已清空的节点,保持 head 指向有效或哨兵节点)。 - 失败:说明已被其他线程抢先出队,继续向后查找。
- 成功:逻辑上移除该元素,返回原值;随后判断是否需
- 注意:
head不一定指向实际第一个元素,可能指向已清空的哨兵或中间节点,只要能通过succ()方法(跳过 item 为 null 的节点)到达首个有效节点即可。
松弛更新(slack)与不变式
-
head和tail不每步都更新,而是允许“滞后”。比如 tail 可能落后真实尾部多个节点,只要最终能通过succ()在 O(1) 或摊还 O(1) 时间内定位到末尾即可。 - 核心不变式:
- 所有存活元素(
item != null)都可从head出发,经succ()(跳过item == null的节点)访问到; - 有且仅有一个
next == null的节点,它是物理尾部,入队操作只在这个节点上 CASnext; -
casItem(x, null)成功即代表该节点逻辑上已出队,即使它仍挂在链表中。
- 所有存活元素(
注意几个易忽略的细节
-
size()不是常量时间:需遍历链表统计item != null的节点数,且遍历时其他线程可能正在修改,结果仅是快照。 - 迭代器弱一致性:构造时拍下链表快照,不反映后续增删,也不抛
ConcurrentModificationException。 - 不接受
null元素:checkNotNull(e)在offer中直接校验,避免与哨兵节点语义混淆。 -
lazySetNext()用于非关键路径的next设置(如构造批量数据时),提供比 volatile write 更轻量的有序写入。
不复杂但容易忽略。

















