HOPS 是避免 CAS 竞争失控的必要妥协,允许 tail 滞后真实尾节点最多 1 个位置,通过异步尽力更新 tail 来换取高吞吐,而非优化技巧。

ConcurrentLinkedQueue 的 HOPS 不是“优化技巧”,而是避免 CAS 竞争失控的必要妥协——它让 tail 指针可以滞后,从而把高频率的写操作从“每次都要更新 tail”降级为“偶尔才更新”。
为什么 tail 指针经常不指向真实尾节点?
tail 不是“懒”,是设计使然。ConcurrentLinkedQueue 允许 tail 滞后于实际尾节点最多 1 个位置(即 tail → A → B,而真实尾节点是 B,但 tail 仍指向 A)。这种滞后由 HOPS 控制,默认阈值是 HOPS = 1。
这样做能大幅减少对 tail 字段的 CAS 更新次数:入队时只要能成功设置 p.next = n 就算完成,只有当“已跳过 HOPS 次检查且发现 tail 明显落后”时,才尝试用 casTail(t, n) 推进 tail。
- 若每次入队都强制更新 tail,多线程下
casTail会高频失败重试,拖慢整体吞吐 - tail 滞后不影响正确性:后续入队线程通过
succ(p)能自然遍历到真实尾部 - tail 滞后也不影响 poll():出队只依赖 head,与 tail 无关
HOPS 如何影响 offer() 的实际执行路径?
看源码里的 retry 循环和 hops 计数器,就能理解它怎么分流操作压力:
- 第一次遍历到尾节点(
next == null),直接p.casNext(null, n),成功就返回 - 如果此时
hops >= HOPS(比如已跳过 1 次中间节点),再补一次casTail(t, n)—— 这次允许失败,不阻塞主流程 - 若遍历中发现
t != tail(其他线程刚推进了 tail),就放弃当前 tail 快照,跳回retry重新开始
也就是说,HOPS 把“维护 tail 准确性”这件事,从同步强一致降级为异步尽力而为。这正是非阻塞算法里典型的“用弱一致性换高吞吐”权衡。
手写无锁队列时最容易误用 HOPS 的三个地方
很多人抄了 hops 变量却没吃透语义,结果掉坑里:
- 把
HOPS设成 0:相当于强制每次入队都更新 tail,CAS 冲突率飙升,性能反而比不用 HOPS 还差 - 在
casTail失败后抛异常或阻塞重试:违背非阻塞前提,offer()就不再是 O(1) 平均时间复杂度 - 忽略
succ(p)对自引用(p == p.next)的处理:这是哨兵节点被回收或初始化时的特殊状态,跳过它才能安全遍历,否则无限循环
真正关键的不是 HOPS 数值本身,而是它背后那个判断:**什么时候该把“维护结构整洁”的成本,转嫁给“后续操作者”去承担?** ConcurrentLinkedQueue 的答案很务实——只要不破坏原子性、不导致死循环、不泄漏内存,就尽量往后推。这也是为什么它的 offer() 在百万级 TPS 下依然稳定,而手写版本往往在千级并发就卡住。细节不在代码行数,而在每一次 CAS 前,你有没有想清楚:这次更新,真的非做不可吗?










