在 Java 并发编程体系中,线程安全队列是实现生产者 - 消费者模式、任务分发、流量缓冲、线程解耦的核心组件,也是 java.util.concurrent(JUC)包的核心基石。根据实现机制的不同,线程安全队列可分为两大流派:基于锁与条件变量的阻塞队列 ,以及基于 CAS 原子指令的非阻塞队列。
本文将从设计思想、核心实现、源码细节三个维度,系统拆解 JDK 1.8 中全部核心线程安全队列,深度剖析 ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue 等高频面试实现的底层原理,并整理一线大厂高频面试题与源码级答案,兼具工程实践与面试备考价值。
一、线程安全队列的两大实现范式
1.1 阻塞算法队列
阻塞队列的核心是「锁 + 条件等待」机制,通过锁保证队列操作的原子性,通过条件变量实现线程的阻塞与唤醒。
- 当队列已满时,插入元素的线程会被阻塞挂起,直到队列出现空位被唤醒;
- 当队列为空时,获取元素的线程会被阻塞挂起,直到队列存入新元素被唤醒。
根据锁的粒度设计,又可分为两种实现模式:
- 单锁模式 :入队和出队共用一把锁,同一时刻只能有一个线程执行读写操作,实现简单但并发度较低,代表实现为
ArrayBlockingQueue; - 双锁模式 :入队和出队分别使用独立的锁,头尾操作互不干扰,入队与出队可并行执行,并发吞吐量更高,代表实现为
LinkedBlockingQueue。
1.2 非阻塞算法队列
非阻塞队列完全摒弃锁机制,基于 CAS(Compare-And-Swap)原子指令 + 自旋重试 实现线程安全。在高并发场景下,避免了锁带来的上下文切换、线程阻塞与死锁风险,通常具备更优的吞吐量;但算法设计复杂度更高,且天然为无界实现。
Java 中最具代表性的非阻塞队列是 ConcurrentLinkedQueue,也是 JUC 包中无锁算法的经典实现。
二、阻塞队列体系全景与核心 API
2.1 BlockingQueue 核心方法语义
BlockingQueue 是所有阻塞队列的顶级接口,针对入队、出队操作提供了 4 类不同语义的方法,是并发编程与面试的基础必考点:
| 操作类型 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
|---|---|---|---|---|
| 入队 | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
| 出队 | remove() |
poll() |
take() |
poll(time, unit) |
| 检查队首 | element() |
peek() |
不支持 | 不支持 |
- 抛出异常 :队列满时
add抛出IllegalStateException,队列空时remove/element抛出NoSuchElementException; - 返回特殊值 :
offer成功返回true、失败返回false,poll/peek空队列返回null,不阻塞线程; - 一直阻塞 :队列满时
put阻塞直到有空位,队列空时take阻塞直到有元素,全程响应线程中断; - 超时退出:阻塞等待指定时长,超时仍未成功则返回特殊值退出,避免永久阻塞。
2.2 JUC 全量阻塞队列实现盘点
JDK 1.8 的 JUC 包中提供了 7 种主流阻塞队列实现,各自适配不同业务场景:
- ArrayBlockingQueue:基于数组实现的有界阻塞队列,单锁 + 双条件变量,支持公平 / 非公平模式,初始化必须指定容量,无法扩容。
- LinkedBlockingQueue :基于单向链表实现的可选有界阻塞队列,默认容量为
Integer.MAX_VALUE,采用双锁设计,入队出队可并行,吞吐量显著高于ArrayBlockingQueue。 - PriorityBlockingQueue :支持优先级排序的无界阻塞队列,底层基于二叉堆实现,元素需实现
Comparable接口,单锁保证线程安全。 - DelayQueue :支持延迟获取元素的无界阻塞队列,元素必须实现
Delayed接口,底层基于优先级队列排序,只有延迟到期的元素才能被取出。 - SynchronousQueue :不存储元素的阻塞队列,每个插入操作必须等待另一个线程的移除操作,反之亦然,支持公平 / 非公平模式,是
CachedThreadPool线程池的默认队列。 - LinkedTransferQueue :基于链表的无界传输队列,继承了直接传递能力,同时支持普通队列操作,
transfer方法可确保元素被消费者接收后再返回。 - LinkedBlockingDeque:基于双向链表的双端阻塞队列,支持两端入队出队,采用单锁设计,可用于工作窃取算法场景。
三、高频阻塞队列源码深度解析(JDK 1.8)
3.1 ArrayBlockingQueue:单锁双条件的经典范本
ArrayBlockingQueue 是理解阻塞队列机制的最佳范本,也是面试中阻塞队列基础原理的核心考察点,基于环形数组实现,采用单锁保护全部读写操作。
核心数据结构
java
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
// 存储元素的数组,final保证引用不可变
final Object[] items;
// 下一个出队元素的数组索引
int takeIndex;
// 下一个入队元素的数组索引
int putIndex;
// 队列中元素总数
int count;
// 全局重入锁,所有读写操作都需要先获取这把锁
final ReentrantLock lock;
// 非空条件:队列为空时,出队线程在此等待
private final Condition notEmpty;
// 非满条件:队列满时,入队线程在此等待
private final Condition notFull;
}
设计要点:单锁保护所有共享变量,因此
count、索引变量都不需要volatile或原子类,锁的 happens-before 规则即可保证可见性与原子性。
构造与公平性设计
java
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
// 根据 fair 参数创建公平/非公平锁
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
- 公平模式:锁按照线程请求顺序分配,等待最久的线程优先获取锁,避免饥饿,但性能略低;
- 非公平模式(默认):线程可插队抢占锁,吞吐量更高,极端情况下可能出现线程饥饿。
阻塞入队 put () 源码
java
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 可中断加锁,响应线程中断
lock.lockInterruptibly();
try {
// 循环判断队列是否已满,防止虚假唤醒
while (count == items.length)
notFull.await(); // 队列满,释放锁,进入阻塞等待
// 执行入队核心逻辑
enqueue(e);
} finally {
lock.unlock();
}
}
// 私有入队方法
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
// 环形数组:索引到数组末尾则回到0,实现空间复用
if (++putIndex == items.length)
putIndex = 0;
count++;
// 唤醒一个等待非空条件的出队线程
notEmpty.signal();
}
关键细节:必须用 while 循环判断队列状态,而不是 if。线程被唤醒后可能其他线程先操作了队列,需要重新检查状态,规避虚假唤醒问题,这是并发编程的标准范式。
阻塞出队 take () 源码
java
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await(); // 队列为空,释放锁,阻塞等待
return dequeue();
} finally {
lock.unlock();
}
}
// 私有出队方法
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null; // 置空帮助GC回收
// 环形索引复位
if (++takeIndex == items.length)
takeIndex = 0;
count--;
// 唤醒一个等待非满条件的入队线程
notFull.signal();
return x;
}
3.2 LinkedBlockingQueue:双锁高并发设计
LinkedBlockingQueue 是实际开发中使用最广泛的阻塞队列,核心考点是双锁设计与并发协作机制,常与 ArrayBlockingQueue 做对比考察。
核心数据结构
java
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
// 链表节点内部类,无volatile修饰(锁保证可见性)
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
// 队列容量,默认 Integer.MAX_VALUE
private final int capacity;
// 元素计数,原子类(双锁下保证线程安全)
private final AtomicInteger count = new AtomicInteger();
// 链表头哨兵节点
transient Node<E> head;
// 链表尾节点
private transient Node<E> last;
// 入队专用锁
private final ReentrantLock putLock = new ReentrantLock();
// 入队等待条件:队列不满
private final Condition notFull = putLock.newCondition();
// 出队专用锁
private final ReentrantLock takeLock = new ReentrantLock();
// 出队等待条件:队列非空
private final Condition notEmpty = takeLock.newCondition();
}
设计要点:
- 入队只修改尾节点,出队只修改头节点,两者操作的数据无重叠,因此可用两把锁分别保护,实现入队出队完全并行;
count使用AtomicInteger,因为入队和出队持不同的锁,需要原子类保证计数的线程安全与可见性;- Node 节点不需要
volatile修饰,因为所有读写都在锁的保护范围内。
阻塞入队 put () 源码
java
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
// 1. 获取入队锁
putLock.lockInterruptibly();
try {
// 循环判断队列是否已满,防止虚假唤醒
while (count.get() == capacity) {
notFull.await();
}
// 2. 节点接入链表尾部
enqueue(node);
// 3. 计数原子+1,返回旧计数值
c = count.getAndIncrement();
// 4. 如果队列还没满,唤醒其他等待入队的线程
if (c + 1 < capacity)
notFull.signal();
} finally {
// 5. 释放入队锁
putLock.unlock();
}
// 6. 旧计数为0,说明之前队列为空,有出队线程在等待,唤醒它们
if (c == 0)
signalNotEmpty();
}
private void enqueue(Node<E> node) {
last = last.next = node;
}
// 唤醒出队线程:必须先获取出队锁,再触发条件唤醒
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
阻塞出队 take () 源码
java
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
// 1. 获取出队锁
takeLock.lockInterruptibly();
try {
// 循环判断队列是否为空
while (count.get() == 0) {
notEmpty.await();
}
// 2. 头节点出队
x = dequeue();
// 3. 计数原子-1,返回旧计数值
c = count.getAndDecrement();
// 4. 如果队列还有元素,唤醒其他等待出队的线程
if (c > 1)
notEmpty.signal();
} finally {
// 5. 释放出队锁
takeLock.unlock();
}
// 6. 旧计数等于容量,说明之前队列已满,有入队线程在等待,唤醒它们
if (c == capacity)
signalNotFull();
return x;
}
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // 旧头节点自引用,帮助GC回收
head = first;
E x = first.item;
first.item = null;
return x;
}
// 唤醒入队线程:必须先获取入队锁
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
核心设计:双锁全程不会同时持有,入队线程先释放 putLock 再获取 takeLock 唤醒,出队线程同理,彻底避免了循环等待,不会产生死锁。
3.3 SynchronousQueue:无缓冲直接传递队列
SynchronousQueue 是面试高频难点,常考察 "无容量队列的实现原理""线程池 CachedThreadPool 的队列选型" 等问题。
核心特性与应用场景
- 队列容量为 0,不存储任何元素,
peek()永远返回null,size()永远返回 0; - 每个
put操作必须等待一个take操作,每个take操作必须等待一个put操作,相当于线程间 "手递手" 直接传递数据; - 支持公平模式(FIFO 队列)和非公平模式(LIFO 栈),默认非公平模式,吞吐量更高;
- 典型应用:
Executors.newCachedThreadPool()的默认任务队列,实现任务的即时提交与即时执行。
底层架构:Transferer 双实现
SynchronousQueue 内部通过 Transferer 抽象接口定义核心传递逻辑,对应两种实现:
java
abstract static class Transferer<E> {
// 核心传递方法:e为null表示消费者取数据,e非null表示生产者放数据
abstract E transfer(E e, boolean timed, long nanos);
}
// 非公平模式:栈结构,LIFO
static final class TransferStack<E> extends Transferer<E> { ... }
// 公平模式:队列结构,FIFO
static final class TransferQueue<E> extends Transferer<E> { ... }
非公平模式 TransferStack 核心原理
默认的非公平模式基于栈实现,核心逻辑如下:
- 线程调用
transfer时,先判断栈顶节点类型是否与自己匹配(生产者匹配消费者); - 若不匹配或栈为空,则将自身封装成节点压入栈,自旋 / 阻塞等待匹配;
- 若匹配,则弹出栈顶节点完成数据交换,返回结果;
- 节点有三种状态:
REQUEST(消费者)、DATA(生产者)、FULFILLING(匹配中)。
设计要点:非公平模式下后到的线程先匹配(栈顶优先),减少了线程唤醒的上下文切换开销,因此吞吐量更高,但可能导致先到的线程长期得不到匹配,产生饥饿。
四、非阻塞队列标杆:ConcurrentLinkedQueue 无锁设计
ConcurrentLinkedQueue 是基于单向链表实现的无界线程安全队列,严格遵循 FIFO 原则,全程采用无锁 CAS 操作,是 JUC 包中非阻塞算法的经典实现。
4.1 核心设计思想
head头节点与tail尾节点均使用volatile修饰,保证多线程间的内存可见性;- 头尾节点采用**惰性更新(延迟更新)**策略:并不每次操作都更新头尾指针,通过减少 CAS 写操作的次数来降低竞争开销;
- 节点内部的
item元素与next指针均为volatile修饰,配合 CAS 操作保证节点修改的原子性。
4.2 核心数据结构(JDK 1.8)
java
public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
implements Queue<E>, java.io.Serializable {
// 头节点,volatile保证多线程可见性
private transient volatile Node<E> head;
// 尾节点,volatile保证多线程可见性
private transient volatile Node<E> tail;
// 构造函数:初始化时head和tail共同指向一个空哨兵节点
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
// 链表节点内部类
private static class Node<E> {
volatile E item;
volatile Node<E> next;
// CAS原子更新节点元素
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
// CAS原子更新next指针
boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
void lazySetNext(Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}
}
}
初始状态下
head和tail指向同一个空哨兵节点(item 为 null),目的是简化边界条件处理,避免入队出队时频繁判断空队列场景。
4.3 入队操作 offer () 源码解析
入队的核心逻辑:从 tail 出发向后遍历找到真正的尾节点,通过 CAS 将新节点接入链表;不会每次入队都更新 tail 节点,只有当 tail 与实际尾节点存在偏移时才尝试更新。
java
public boolean offer(E e) {
checkNotNull(e); // 不允许插入null元素
final Node<E> newNode = new Node<E>(e);
// 自旋死循环,入队失败则不断重试
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
// 情况1:p是真正的尾节点(next为null)
if (q == null) {
// CAS将p的next指针指向新节点
if (p.casNext(null, newNode)) {
// 核心优化:p不等于t,说明tail不是真正的尾节点,存在跳跃
// 此时才尝试更新tail到新尾节点,更新失败也不影响正确性
if (p != t)
casTail(t, newNode);
return true;
}
// CAS失败,说明其他线程先完成了入队,自旋重试
}
// 情况2:p == q,遇到自引用的旧哨兵节点,需要重新定位
else if (p == q)
// tail若已被更新则用新tail,否则跳回head重新遍历
p = (t != (t = tail)) ? t : head;
// 情况3:p不是尾节点,继续向后遍历
else
// tail若已被其他线程更新则跳转新tail,否则继续走next
p = (p != t && t != (t = tail)) ? t : q;
}
}
设计精髓:tail 节点的惰性更新
这是典型的用读开销换写开销的性能优化:
- 更新
tail节点需要执行一次 CAS 写操作,高并发下 CAS 竞争激烈,开销较大; - 如果每次入队都更新
tail,每个入队线程都会竞争 tail 的 CAS 锁,冲突严重; - 设计上允许
tail滞后真实尾节点最多 1 个位置,每两次入队才执行一次 tail 更新,大幅减少 CAS 操作的总次数; - 代价只是入队时多做一次 volatile 读(遍历 next 指针),而 volatile 读的开销远低于 CAS 写,整体吞吐量显著提升。
4.4 出队操作 poll () 源码解析
出队与入队的设计思想完全一致:head 节点同样采用惰性更新,并不是每次出队都移动 head 指针,而是隔一次更新一次,核心目的也是减少 CAS 操作。
java
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
// 情况1:当前节点有有效元素,CAS将item置为null(逻辑删除)
if (item != null && p.casItem(item, null)) {
// p不等于head,说明跳过了哨兵节点,需要更新head
if (p != h)
// 更新head:有next则指向next,否则指向自身
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
// 情况2:已遍历到队尾,队列为空,返回null
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
// 情况3:p自引用,说明head已被其他线程更新,重新从head开始
else if (p == q)
continue restartFromHead;
// 情况4:继续向后遍历找有效节点
else
p = q;
}
}
}
// 更新head节点,并将旧head的next指向自己(帮助GC回收)
final void updateHead(Node<E> h, Node<E> p) {
if (h != p && casHead(h, p))
h.lazySetNext(h); // 旧head自引用,成为哨兵标记,同时帮助GC
}
出队逻辑核心要点
- 逻辑删除机制 :出队并不是把节点从链表中物理移除,而是通过 CAS 将节点的
item置为 null 完成逻辑删除; - 哨兵节点机制 :
head始终指向一个 item 为 null 的哨兵节点,真正的第一个有效元素在head.next; - 惰性更新 head:每次出队成功后,只有当出队节点偏离 head 时才更新 head 指针,减少 CAS 写操作;
- 自引用辅助 GC:旧 head 被设置为自引用(next 指向自己),既作为遍历中的哨兵标记,也帮助 GC 更快回收失效节点。
4.5 关键设计细节:ABA 问题处理
无锁算法普遍面临 ABA 问题,ConcurrentLinkedQueue 通过以下机制规避
- 节点不复用:出队的节点不会重新入队,每个新元素都会创建新的 Node 对象,避免了节点内存地址复用导致的 ABA;
- 自引用标记:失效的头节点会设置 next 指向自己,作为哨兵标记,遍历时遇到自引用节点会重新从 head 开始,避免遍历到旧链表;
- volatile 语义 :所有共享变量都是
volatile修饰,保证引用更新的内存可见性,避免指令重排导致的状态不一致
五、核心队列横向对比与选型指南
5.1 阻塞队列 vs 非阻塞队列
| 对比维度 | 阻塞队列 | 非阻塞队列(ConcurrentLinkedQueue) |
|---|---|---|
| 实现原理 | 锁 + Condition 条件等待 | CAS 原子指令 + 自旋重试 |
| 队列边界 | 多为有界(如 ArrayBlockingQueue) | 天然无界 |
| 阻塞特性 | 支持阻塞等待,可超时 | 不支持阻塞,立即返回结果 |
| 并发性能 | 有锁竞争,存在上下文切换开销 | 高并发下吞吐量更优 |
| 适用场景 | 生产者 - 消费者模式、流量削峰、线程解耦 | 高并发低延迟场景、非阻塞任务分发 |
| 代表实现 | ArrayBlockingQueue、LinkedBlockingQueue、DelayQueue | ConcurrentLinkedQueue |
5.2 工程选型建议
- 需要阻塞等待、流量削峰 :优先选择阻塞队列,有界场景选
ArrayBlockingQueue,高并发场景选LinkedBlockingQueue; - 高并发非阻塞、追求极致吞吐量 :选择
ConcurrentLinkedQueue,注意控制生产速度避免内存溢出; - 延迟任务、定时调度 :选择
DelayQueue; - 线程池零缓存、即时执行 :选择
SynchronousQueue,对应 CachedThreadPool 场景。
六、一线大厂高频面试题与源码级答案
- ArrayBlockingQueue 和 LinkedBlockingQueue 有哪些核心区别?
| 维度 | ArrayBlockingQueue | LinkedBlockingQueue |
|---|---|---|
| 底层结构 | 数组实现,有界,初始化必须指定容量 | 单向链表实现,可选有界,默认近似无界 |
| 锁设计 | 单锁,入队出队共用一把锁,无法并行 | 双锁,入队出队各一把锁,可并行执行 |
| 计数方式 | 普通 int 变量,锁保护下操作 | AtomicInteger 原子类,双锁下保证安全 |
| 内存开销 | 预分配数组,无额外节点开销 | 每个元素对应一个 Node 对象,内存开销大 |
| 吞吐量 | 较低,锁竞争激烈 | 较高,入队出队并行执行 |
| GC 压力 | 低,数组空间复用,无临时对象 | 高,频繁创建销毁 Node 对象 |
2. 阻塞队列中为什么要用 while 循环判断等待条件,而不是 if?
核心是为了规避虚假唤醒 问题。 操作系统层面,Condition.await() 方法可能在没有被 signal() 调用的情况下自发唤醒(虚假唤醒),属于操作系统的正常现象。如果使用 if 判断,线程被虚假唤醒后会直接执行后续逻辑,此时队列可能仍然满 / 空,导致逻辑错误。 使用 while 循环时,线程被唤醒后会重新检查条件是否满足,不满足则继续等待,保证了逻辑的正确性,这是并发编程的标准范式。
3. ConcurrentLinkedQueue 的 tail 节点为什么不总是指向真正的尾节点?这样设计有什么好处?
这是一种**惰性更新(延迟更新)**的性能优化策略。
- 更新
tail节点需要执行 CAS 写操作,而 CAS 写在高并发下竞争激烈,开销远大于 volatile 读; - 如果每次入队都更新 tail,每个入队线程都会竞争 tail 的 CAS 锁,冲突严重;
- 设计上允许 tail 滞后真实尾节点最多 1 个位置,每两次入队才执行一次 tail 更新,大幅减少 CAS 操作的总次数;
- 代价只是入队时多做一次 next 指针的 volatile 读,整体吞吐量显著提升。
4. DelayQueue 的延迟出队是怎么实现的?
DelayQueue 底层基于 PriorityQueue 优先级队列实现,核心逻辑:
- 所有元素必须实现
Delayed接口,重写getDelay()返回剩余延迟时间,compareTo()定义排序规则; - 元素入队时按延迟时间从小到大排序,队首元素是最早到期的;
- 出队时(
take方法),先获取队首元素,判断getDelay()是否 <=0:已到期则直接出队;未到期则调用awaitNanos()阻塞等待剩余的延迟时间,时间到后自动唤醒重试; - 内部使用单锁保证线程安全。
5. 为什么线程池 CachedThreadPool 选择使用 SynchronousQueue?
CachedThreadPool 的设计目标是核心线程数为 0、最大线程数无上限,任务提交后立即执行,SynchronousQueue 完美贴合这一目标:
SynchronousQueue不缓存任务,提交任务时直接尝试将任务交给空闲线程;- 如果没有空闲线程,队列入队失败,线程池会立即创建新线程处理任务,实现 "零等待" 的弹性扩容;
- 如果使用有界队列,任务会排队等待,无法实现任务立即执行的设计目标
6. ConcurrentLinkedQueue 的 size () 方法为什么不推荐频繁调用?
- 性能差 :
size()需要从 head 开始遍历整个链表统计元素数量,时间复杂度为 O (n),队列元素越多性能损耗越大; - 结果不精确:遍历过程中没有加锁,并发场景下队列可能持续增删元素,最终返回的只是一个近似值,不具备精确的实时语义;
- 高并发场景下如果需要判断队列是否为空,推荐使用
isEmpty()方法,只需判断第一个有效节点是否存在,性能远高于size()。
7. CAS 实现无锁队列的核心难点是什么?
CAS 实现无锁队列的核心难点主要有三个:
- 头尾节点的并发一致性:多线程同时入队 / 出队时,如何保证头尾指针更新的原子性和一致性,避免链表断裂;
- ABA 问题 :节点被删除后又被复用,可能导致 CAS 误判。
ConcurrentLinkedQueue通过节点不复用、自引用标记等方式规避 ABA 问题; - 惰性更新的正确性:为了性能采用头尾节点延迟更新,需要保证并发场景下任意线程遍历总能找到正确的头尾节点,不会出现死循环或遍历丢失。
8. LinkedBlockingQueue 为什么要设计两把锁?相比单锁有什么优势?
LinkedBlockingQueue 设计了 putLock(入队锁)和 takeLock(出队锁)两把独立的锁,分别控制链表的头尾操作。 核心优势是提升并发度:链表结构下入队只修改尾节点、出队只修改头节点,两者不存在数据竞争,使用两把锁可以让生产者线程和消费者线程完全并行工作,大幅提升队列的并发吞吐量。 代价是实现逻辑更复杂,且需要同时维护两把锁的状态,同时通过 "先释放锁、再获取另一把锁唤醒" 的顺序规避死锁。
七、总结
线程安全队列是 Java 并发编程的核心组件,阻塞队列凭借简洁的语义和可靠的阻塞机制,成为生产者 - 消费者场景的首选;而非阻塞队列依托 CAS 无锁算法,在高并发低延迟场景下具备不可替代的性能优势。