ConcurrentLinkedQueue 用 CAS 配合 volatile 实现无锁线程安全:CAS 更新 tail 和 head 指针,volatile 保证可见性与有序性;size() 非实时,不可用于条件判断,且不支持 null 元素。

Java 中无锁队列(如 ConcurrentLinkedQueue)不靠 synchronized 或 ReentrantLock,而是用 CAS(Compare-And-Swap)配合 volatile 字段实现线程安全。它的核心不是“没同步”,而是把同步逻辑下沉到 CPU 指令级,靠乐观重试代替阻塞等待。
CAS 在队列里具体干了什么
CAS 不是泛泛而谈的机制,它在队列中只作用于两个关键位置:
- tail 指针更新:入队时,线程尝试把新节点设为当前 tail 的 next;若失败(tail 已被别的线程改过),就先用一次 CAS 把 tail 推进到更靠后的节点,再重试连接
- head 指针更新:出队时,线程先读 head.next,再 CAS 将 head 指向该 next 节点;若发现 head 已经“卡住”在傀儡节点上,就推进 head 并继续找真实数据节点
为什么光有 CAS 还不够?volatile 是怎么配合的
CAS 操作本身能保证单次更新的原子性,但要让整个队列结构对所有线程可见、不乱序,必须靠 volatile:
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
-
head和tail字段声明为volatile,确保每次读取都从主内存拿最新值,写入立即刷回 - JVM 在
volatile读写前后插入内存屏障,防止编译器或 CPU 把item = x重排到next = newNode之后——这会破坏节点发布的安全性 - Node 内部的
item和next虽未显式加volatile,但通过Unsafe.compareAndSetObject对引用字段的原子操作,间接实现了安全发布
线程安全不是“绝对正确”,而是“弱一致性下的可预测”
无锁队列的线程安全有明确边界,理解这些才能避免误用:
立即学习“Java免费学习笔记(深入)”;
-
offer()总返回true,poll()空时返回null,但poll() == null不代表队列真为空——可能只是当前线程看到的head还没推进到位 -
size()需遍历链表,结果非实时;不能用它做空判断,应优先用isEmpty() - 不允许存
null,否则无法区分“队列空”和“存了个 null” - 高并发下多个线程反复 CAS 失败会自旋重试,CPU 使用率可能升高,但不会挂起线程或引发死锁
实际使用时的关键提醒
直接 new 一个 ConcurrentLinkedQueue 很简单,但真正用好它需要避开几个典型坑:
- 别拿
size()做条件判断,比如if (q.size() > 0) q.poll()是竞态的,应改用if (!q.isEmpty()) q.poll() - 不要在循环里反复调用
size(),尤其在日志缓冲等高频场景,它开销明显 - 它适合写多读少、或生产者/消费者数量动态变化的场景,不适合要求强一致 size 或严格 FIFO 顺序校验的业务
- 如果业务需要阻塞语义(比如取不到就等),别硬套它,应选
LinkedBlockingQueue等带锁队列

















