ConcurrentLinkedQueue适合高并发、低争用、不关心顺序一致性的场景,如日志收集、异步任务缓冲;add()与offer()无实质区别,均基于CAS循环,建议统一用offer();size()不可靠且为O(n),应避免用于循环条件。

ConcurrentLinkedQueue 适合什么场景
它只适合「高并发、低争用、不关心顺序一致性」的生产者-消费者模型。比如日志收集、异步任务缓冲、事件总线的中间队列——这些地方允许少量乱序,且写入远多于遍历。
别把它当 ArrayList 用,也别指望它支持 contains() 或 remove(Object) 的高效调用;这些操作是 O(n) 且会遍历整个链表,实际中几乎等于自废武功。
add() 和 offer() 有区别吗
没有实质区别:两者都调用同一段 CAS 循环逻辑,都是无阻塞、永不抛异常、始终返回 true。JDK 文档写 add() 可能抛 IllegalStateException,但 ConcurrentLinkedQueue 内部容量无限,这个异常永远触发不了。
建议统一用 offer(),语义更准确——毕竟你不是在“添加”,而是在“尝试入队”,只是这个尝试永远不会失败。
-
add()和offer()底层共用casTail()+ 自旋重试 -
poll()在队列空时直接返回null,不会阻塞或重试 - 不要用
element()或peek()判断是否为空——它们返回null不代表队列空(可能真存了null元素,虽然不推荐)
为什么 size() 返回的值不可靠
因为它是遍历链表计数,过程中其他线程可能正在增删节点,结果既不是快照也不是原子值。某次调用返回 1024,下一行就可能发现 poll() 立刻返回 null。
真实业务里几乎不需要知道“当前精确长度”。如果非要感知积压,改用带计数器的封装类,或者直接监控下游消费延迟。
-
size()时间复杂度是 O(n),并发越高越慢 -
isEmpty()虽然快(只比对 head/tail),但也可能刚返回true就被另一个线程插入 - 别在循环条件里写
while (queue.size() > 0)—— 这是典型误用,应改用poll() != null
空指针和内存泄漏风险在哪
主要来自两处:一是存了 null 元素(ConcurrentLinkedQueue 允许,但 poll() / peek() 返回 null 时无法区分“队列空”还是“存的就是 null”);二是节点未被及时 GC,当生产者远快于消费者,大量节点长期驻留堆中。
前者靠编码约定规避(禁止放入 null);后者得靠背压机制控制,比如结合 Semaphore 限流,或改用有界队列如 LinkedBlockingQueue。
- 存
null后,poll()返回null意义模糊,容易引发 NPE 或逻辑错判 - 节点对象本身不持有强引用,但若队列长期不消费,GC 无法回收已出队节点的前驱引用(因内部使用 volatile tail/head,存在弱引用链)
- JDK 9+ 对节点清理做了优化,但老版本(如 JDK 8)在极端长生命周期队列中仍可能观察到内存缓慢上涨
事情说清了就结束。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











