ConcurrentLinkedQueue 与 CopyOnWriteArrayList:无锁并发容器原理
文章目录
《ConcurrentLinkedQueue 与 CopyOnWriteArrayList:无锁并发容器原理》
一、前言:为什么要“无锁容器”
传统容器如 ArrayList、LinkedList 在并发访问时需要加锁,
而 Vector、Collections.synchronizedList() 虽然线程安全,
但由于锁粒度大、阻塞严重,在高并发环境下性能低下。
于是,JUC(java.util.concurrent)提供了一系列 无锁(Lock-Free)容器,
依赖 CAS(Compare-And-Swap)原子操作 来保证线程安全。
本篇重点讲:
- ConcurrentLinkedQueue → 无锁链表队列
- CopyOnWriteArrayList → 写时复制列表
这两者分别代表了:
一类是基于 CAS 的无锁并发结构,
一类是基于 读写分离的快照思想。
二、ConcurrentLinkedQueue:CAS 无锁队列
(1)设计思想
ConcurrentLinkedQueue 是一个 无界非阻塞队列,
基于 链表 + CAS + 自旋 实现线程安全。
特点:
- 采用 FIFO(先进先出);
- 不加锁,性能优于
LinkedBlockingQueue; - 适用于高并发、短任务的场景。
(2)核心结构
public class ConcurrentLinkedQueue<E> {
private transient volatile Node<E> head;
private transient volatile Node<E> tail;
private static class Node<E> {
volatile E item;
volatile Node<E> next;
}
}
每个节点保存元素和 next 指针,
head 指向队首,tail 指向队尾。
(3)入队(offer)源码简化
public boolean offer(E e) {
Node<E> newNode = new Node<>(e, null);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) { // p为队尾
if (p.casNext(null, newNode)) { // CAS 插入
if (p != t)
casTail(t, newNode); // 尝试更新尾节点
return true;
}
} else if (p == q)
p = (t != (t = tail)) ? t : head; // tail 落后时推进
else
p = q; // 向后遍历
}
}
核心机制:
- 通过自旋 + CAS 确认尾节点;
- 线程安全地插入新节点;
- 失败则重试(无锁自旋)。
(4)出队(poll)源码简化
public E poll() {
for (Node<E> h = head, p = h;;) {
E item = p.item;
if (item != null && p.casItem(item, null)) {
if (p != h)
updateHead(h, p.next);
return item;
} else if ((p = p.next) == null)
return null;
}
}
特点:
- 出队同样基于 CAS;
- 删除节点后会尝试推进 head;
- 保证无锁状态下的安全性。
(5)应用场景
- 高并发日志队列
- 异步事件消息缓冲
- 无阻塞任务收集
如果需要“阻塞功能”,应使用
LinkedBlockingQueue;
如果追求“无锁高吞吐”,则用ConcurrentLinkedQueue。
三、CopyOnWriteArrayList:写时复制列表
(1)核心思想
Copy-On-Write(COW,写时复制)是一种 读写分离机制:
写操作时复制原数组副本,修改完成后再替换。
读操作始终访问旧副本,不加锁。
(2)核心结构
public class CopyOnWriteArrayList<E> implements List<E> {
private transient volatile Object[] array;
final transient ReentrantLock lock = new ReentrantLock();
}
(3)写操作示例
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
写操作时复制一份新数组,不影响其他线程读取旧数据。
(4)读操作示例
public E get(int index) {
return (E) getArray()[index];
}
读操作直接访问
volatile array,无锁、线程安全。
(5)优缺点对比
| 优点 | 缺点 |
|---|---|
| 读操作无锁,性能高 | 写操作代价大(复制数组) |
| 读写分离,线程安全 | 不适合频繁写入场景 |
| 避免 ConcurrentModificationException | 占用更多内存 |
(6)适用场景
- 读多写少(典型应用如缓存快照、监听器列表);
- Spring 中的事件监听器容器(
ApplicationListener); - 系统白名单、配置列表等高读取低更新数据。
四、ConcurrentLinkedQueue vs CopyOnWriteArrayList
| 对比项 | ConcurrentLinkedQueue | CopyOnWriteArrayList |
|---|---|---|
| 数据结构 | 链表 | 数组 |
| 并发策略 | CAS 无锁 | 写时复制(ReentrantLock) |
| 是否阻塞 | 否 | 否 |
| 读写性能 | 读写均快 | 读快写慢 |
| 适用场景 | 频繁入队出队(任务流) | 读多写少(配置快照) |
| 底层机制 | CAS + 自旋 | 数组快照 + volatile |
| 是否支持随机访问 | 否 | 是 |
五、面试高频问题与答题模板
| 问题 | 答案要点 |
|---|---|
| Q1:ConcurrentLinkedQueue 如何保证线程安全? | 基于 CAS 原子操作和自旋机制实现无锁入队出队。 |
| Q2:ConcurrentLinkedQueue 与 BlockingQueue 区别? | 前者无锁非阻塞,后者带锁阻塞。 |
| Q3:CopyOnWriteArrayList 为什么读不加锁? | 读操作访问 volatile 数组引用,内存可见性由 JVM 保证。 |
| Q4:CopyOnWriteArrayList 为什么写要加锁? | 确保复制数组过程原子,防止多个写线程同时复制。 |
| Q5:CopyOnWriteArrayList 适合什么场景? | 读多写少,如系统配置、白名单、监听器列表。 |
| Q6:为什么 CopyOnWrite 不会抛 ConcurrentModificationException? | 因为读操作访问快照副本,写操作修改新数组,不影响旧数组遍历。 |
| Q7:无锁并发结构为什么性能高? | 因为不阻塞线程,通过 CAS 实现乐观并发控制。 |
六、结语
ConcurrentLinkedQueue 和 CopyOnWriteArrayList 是 无锁并发编程 的两种经典思路:
- 前者体现了 CAS + 链表 + 自旋 的高并发入队出队;
- 后者体现了 读写分离 + 快照副本 的高效读性能。
掌握这两种机制,就掌握了 Java 并发容器设计的核心理念:
“无锁不等于不安全,
而是通过原子性 + 可见性,实现更高层次的性能与安全平衡。”
下一篇,我将写——
《JVM 内存结构与 GC 调优全景图》,
进入 JVM 调优模块,帮你彻底吃透堆、栈、方法区、GC 流程与调优策略。
更多推荐

所有评论(0)