
ConcurrentLinkedQueue 的“无锁”不是没有同步,而是不用传统锁(如 synchronized 或 ReentrantLock),全部靠 CAS 原子操作 配合 volatile 可见性来完成线程安全。它的核心就两件事:让多个线程能并发修改链表,且不互相阻塞、不丢数据、不破坏结构。
关键结构:傀儡头尾节点 + volatile 链式节点
队列初始化时,head 和 tail 都指向同一个 item 为 null 的傀儡节点(也叫哨兵节点)。这不是 bug,是设计前提:
Node 节点中 item 和 next 都用 volatile 修饰,保证多核 CPU 下的内存可见性 所有对 next 指针或 item 值的修改,只通过 UNSAFE.compareAndSwapObject 完成 真实数据节点永远插在傀儡节点之后,head 不直接指向有效元素,tail 也不一定真在队尾
CAS 发生在哪?只有两个关键位置
整个类里,所有线程安全逻辑都压在这两处 CAS 上,其余全是读取和重试:
offer() 时更新 tail:尝试把新节点连到当前 tail.next,失败则先推进 tail 到更靠后的节点再重试 poll() 时更新 head:尝试把 head 推进到下一个节点(跳过已删除或傀儡节点),失败则重读 head 再试
没有锁,没有 wait/notify,没有线程挂起 —— 每次 CAS 失败就立刻重读最新值,然后循环再试。
为什么 offer() 有时要两次 CAS?
入队不是原子一步到位,而是分阶段乐观执行:
第一步:用 CAS 把新节点设为当前 tail.next(假设 tail 正确) 如果失败(说明 tail 已被别的线程改过),就进入第二步:用 CAS 把 tail 推进到它自己的 next 节点(即“帮别人修正尾巴”) 修正完再回到第一步,重新尝试连接
这在高并发写场景下很常见。单线程几乎总是一次成功;10 个线程同时 offer,很可能每个都触发 2~3 轮 CAS 循环。
常见迷惑现象的本质解释
这些不是 bug,而是无锁设计下的自然表现:
poll() 返回 null,但 size() > 0:size() 是遍历计数,poll() 是推进 head;两者完全异步。此时 head 可能还卡在傀儡节点,而第一个数据节点已被其他线程 poll 并标记删除(但 head 尚未推进) tail 不在队尾、head 不指有效元素:这是故意的延迟优化 —— 减少 CAS 冲突。tail 可能停在倒数第二个节点,head 可能长期滞留在傀儡节点 isEmpty() 返回 true,但刚 offer 过元素:因为 head 和 tail 都还没推进到数据节点,此时队列逻辑上仍为空(符合 FIFO 语义)

评论(0)