《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; // 向后遍历
    }
}

核心机制:

  1. 通过自旋 + CAS 确认尾节点;
  2. 线程安全地插入新节点;
  3. 失败则重试(无锁自旋)。
(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

对比项ConcurrentLinkedQueueCopyOnWriteArrayList
数据结构链表数组
并发策略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 流程与调优策略。

更多推荐