前边介绍了 juc 的锁体系 —— ReentrantLock、 ReentrantReadWriteLock 和 StampedLock。
有了锁,从本篇文章开始,开始介绍锁的应用,本篇文章先讨论一种并发容器 —— BlockingQueue。
BlockingQueue :是线程安全的队列,顾名思义,它是个阻塞队列,队列满时,入队线程阻塞;队列空时,出队线程阻塞,常用于生产者-消费者模型。
UML 类图
Queue
⨳ Collection:集合框架的根接口。
从 Collection 直接继承的主要是三个接口:List、Set 和 Queue,不包括 Map。
| 类别 | 方法 | 说明 |
|---|---|---|
| 增删 | add(e)、remove(o)、addAll(c)、removeAll(c)、clear() | 基本增删与批量操作 |
| 查询 | size()、isEmpty()、contains(o)、containsAll(c) | 容量与包含判断 |
| 遍历 | iterator()、stream()、forEach(...) | 迭代器是 Collection 的核心遍历入口 |
Collection 的 抽象实现类 AbstractCollection 就不多说,身为一个抽象类,最大的作用就是首先父接口的大部分通用逻辑。
⨳ Queue :队列语义的接口。
身为一个队列,肯定遵循 FIFO(先进先出) 的通用原则,而且提供了成对出现的“两组”API
| 操作 | 抛异常 | 特殊值 |
|---|---|---|
| 插入 | add(e) | offer(e) |
| 移除 | remove() | poll() |
| 检查 | element() | peek() |
add、remove、element 是操作失败(如队列满或空)时抛异常(IllegalStateException / NoSuchElementException)。
而 offer、poll 与 peek 是“温和版”的操作方法, offer 是尝试放入,poll 是尝试取出,peek 是偷看一眼,放不进去就不放嘛,取不出来就不取嘛,看不到元素就看不到嘛。
AbstractQueue 也不赘述了,主要用来减少队列实现类的重复代码。
BlockingQueue
⨳ BlockingQueue:阻塞队列接口。
普通 Queue 只管“能放就放、能取就取”;BlockingQueue 多了阻塞能力:
| 操作 | 抛异常 | 返回特殊值 | 阻塞 | 阻塞+超时 |
|---|---|---|---|---|
| 插入 | add(e) | offer(e) | put(e) | offer(e,t,u) |
| 移除 | remove() | poll() | take() | poll(t,u) |
| 检查 | element() | peek() | — | — |
对于队列满的情况,add 会抛异常,offer 会返回 false,而 put 则会阻塞,直到队列有空闲或线程被 interrupt() 为止。
对于队列空的情况也差不多,remove 会抛异常,poll 会返回 null,而 take 则会阻塞,直到队列被放进去了值,或线程被 interrupt() 为止。
不仅如此,BlockingQueue 还提供了 put、take 支持超时的版本 —— offer(e,time,unit), poll(time, unit)。
至于为啥 阻塞 的 超时版本使用 返回特殊值 不报错的 offer、poll?这是因为超时后的行为和 offer、poll 一样,也不报错,取不到就就返回 null。
下面看一下,BlockingQueue 有哪些不同的实现,这些不同实现各有什么特点。
ArrayBlockingQueue 基于数组的阻塞队列
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
final Object[] items;
int takeIndex;
int putIndex;
int count;
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
transient Itrs itrs;
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
环形数组
通过看 ArrayBlockingQueue 的成员变量就大致就能推测出工作原理,Object[] items 是个容量固定的底层数组,takeIndex,putIndex 这两个指针前后移动,就可以实现环形数组的效果。
putIndex = (++putIndex == items.length) ? 0 : putIndex;
takeIndex = (++takeIndex == items.length) ? 0 : takeIndex;
为啥不搞个动态扩容的数组呢?
技术上实现,但是 ArrayBlockingQueue 的定位就是有界的阻塞队列,专门用作流量控制的。
守护性暂挂
ArrayBlockingQueue 前文也提到过,标准的 生产者-消费者模式 ,往队列插入数据的守护条件是 notFull,从队列拿数据的守护条件是 notEmpty。
其中 put 和 take 是两个守护性暂挂的方法,put 的时候,如果队列已满,则进入到 notFull 条件队列中等待:
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E e) {
final Object[] items = this.items;
items[putIndex] = e;
if (++putIndex == items.length) putIndex = 0;
count++;
notEmpty.signal();
}
take 的时候,如果队列为空,则进入到 notFull 条件队列中等待:
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 e = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length) takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return e;
}
代码都很简单的,无论是 take 还是 put 都用 ReentrantLock 加锁,条件不满足就等待,条件满足就唤醒。
取元素不需要移动数组,只要移动 takeIndex;放元素也不需要扩容,只要移动 putIndex,所以入队、出队都是 O(1) 。
其他方法不介绍了,都是 ReentrantLock + Condition 这一套。
LinkedBlockingQueue 基于链表的阻塞队列
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
private final int capacity;
private final AtomicInteger count = new AtomicInteger();
transient Node<E> head;
private transient Node<E> last;
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
基于链表的阻塞队列,比基于数组的阻塞队列复杂一点点。
哨兵节点
链表不需要像数组一样,一开始就需要申请连续长度的内存空间,链表因为有指针 next 可以执行下一个节点,空间不需要连续,可以按需分配、动态增长,也就意味着基于链表的阻塞队列可以没有边界的,当然这个所谓的“无边界” 是 Integer.MAX_VALUE(2³¹ − 1)。
队列容量接近 21 亿,当然可能到不了 21 亿,JVM 就提前 OOM 了。
线程池任务队列 Executors.newFixedThreadPool 底层就是它:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
这也是为啥生产环境不推荐直接用 newFixedThreadPool。
需要注意的是,LinkedBlockingQueue 中存放节点的链表是带有 哨兵节点 的,构造链表时, head = last = new Node<>(null),head 本身不存数据,真正的元素从 head.next 开始。
双锁分离
LinkedBlockingQueue 和 ArrayBlockingQueue 最大的不同就是有两把锁。两把锁,锁同一个资源,这不明摆着有并发问题嘛。
其实两把锁,锁的是两个资源。
入队操作 last,出队操作 head.next,两者永远不会操作同一个节点,所以双锁才能成立。
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
final int c;
final Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
private void enqueue(Node<E> node) {
last = last.next = node;
}
因为 head 是哨兵节点,last 永远指向最后一个节点(可能是哨兵也可能是真实数据节点),所以入队永远是尾插,不会和 take 操作 head.next 冲突。
整体流程可以概括成:拿 putLock → 满则等 → 入队 → 计数+1 → 唤醒其他生产者 → 解锁 → 如果从空变非空则跨锁唤醒消费者。
消费者 take 和 put 几乎完全对称:
public E take() throws InterruptedException {
final E x;
final int c;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
把旧哨兵的 next 指向自己,切断对 first 的引用。取出数据后立刻清空引用,避免 GC 无法回收元素对象。
就这样生产者和消费者可以并行执行,这是它吞吐量高于 ArrayBlockingQueue 的根本原因。
你可能会问,当链表为空的时候,生产者和消费者操作的不就是同一个节点了嘛?
当只有哨兵时,head 和 last 确实都指向同一个哨兵节点。此时 count == 0,消费者调用 take 会进入 while (count.get() == 0) notEmpty.await() 阻塞,根本不会执行出队逻辑。所以不会和 put 冲突。
ArrayBlockingQueue 不有也有 takeIndex 和 putIndex 双指针,为啥就不能用双锁呢?
可以是可以?
那得改造,首先 ArrayBlockingQueue 的 count 就不能是 int 类型,要和 LinkedBlockingQueue 一样,使用线程安全的原子类型 AtomicInteger,而且最好也得搞个占位的哨兵头,要不然当容量等于 1 并且队列存了元素时,putIndex == takeIndex,生产者和消费者会操作数组同一个位置 ...
技术上可以实现,但设计者认为不值得,容量有限的情况下,单锁性能也不错。
LinkedBlockingQueue 添加元素时有构造节点的时间,为了减少这部分时间占比,读写锁分离能实现并发优化。
LinkedBlockingDeque 基于链表的双向阻塞队列
从 UIM 类图上,可以看出这个双向队列实现的是 Deque、BlockingDeque。
头尾操作
Deque 全称Double Ended Queue,表示双端队列,普通 Queue 只能尾部入队、头部出队,FIFO 先进先出;而 双端队列 头部、尾部两端都可以插入和删除元素。
而 BlockingDeque 又实现了 BlockingQueue,说明它继承阻塞语义,队空阻塞取元素、队满阻塞放元素,所以它天然同时拥有双端操作 + 阻塞等待两套能力。
| 操作 | 抛异常 | 返回特殊值 | 永久阻塞 | 超时阻塞 |
|---|---|---|---|---|
| 队头插入 | addFirst | offerFirst | putFirst | offerFirst(e,time,unit) |
| 队尾插入 | addLast | offerLast | putLast | offerLast(e,time,unit) |
| 队头取出并删除 | removeFirst | pollFirst | takeFirst | pollFirst(time,unit) |
| 队尾取出并删除 | removeLast | pollLast | takeLast | pollLast(time,unit) |
| 队头查看 (不删) | getFirst | peekFirst | — | — |
| 队尾查看 (不删) | getLast | peekLast | — | — |
BlockingDeque 在 juc 包下只有一个实现类 —— LinkedBlockingDeque。
双向链表
public class LinkedBlockingDeque<E>
extends AbstractQueue<E>
implements BlockingDeque<E>, java.io.Serializable {
static final class Node<E> {
E item;
Node<E> prev;
Node<E> next;
Node(E x) {
item = x;
}
}
transient Node<E> first;
transient Node<E> last;
private transient int count;
private final int capacity;
final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
public LinkedBlockingDeque() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingDeque(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
}
从成员变量 Node 就可以看出来,LinkedBlockingDeque 内部是一个双向链表,而且只有一把全局 ReentrantLock,性能不如 LinkedBlockingQueue。
PriorityBlockingQueue 支持优先级的阻塞队列
public class PriorityBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
private static final int DEFAULT_INITIAL_CAPACITY = 11;
private transient Object[] queue;
private transient int size;
private transient Comparator<? super E> comparator;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private PriorityQueue<E> q;
public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.comparator = comparator;
this.queue = new Object[Math.max(1, initialCapacity)];
}
二叉堆
看成员变量,有个 Object[] queue,感觉这个队列应该是基于数组的有界阻塞队列,其实这个数组是基于数组实现的二叉堆。
二叉堆是一棵完全二叉树,节点从上到下、从左到右连续排列,中间没有空洞,所以天然可以用数组按层序编号。
用数组就得提前确定容量,好分配内存,那这个 PriorityBlockingQueue 也是有界的吗?并不是,这是个支持动态扩容的数组:
// 容量 < 64:翻倍增长(`oldCap + oldCap + 2`)
// 容量 ≥ 64:增长 50%(`oldCap >> 1`)
int growth = (oldCap < 64)
? (oldCap + 2) // grow faster if small
: (oldCap >> 1);
int newCap = ArraysSupport.newLength(oldCap, 1, growth);
if (queue == array)
newArray = new Object[newCap];
而且注意 PriorityBlockingQueue 的成员变量只有一个 notEmpty 条件,没有可以限定生产者可以继续生产的 notFull 条件,为啥呢?
因为是无界队列,入队永远不会阻塞!
可比较优先
那 PriorityBlockingQueue 的 Priority 的优先是啥意思。
这里的“优先”意思是:谁先出队,不由入队顺序决定,而由元素自身的优先级决定。普通队列是 FIFO,先来先走。PriorityBlockingQueue 是,优先级最高者先出队。所以说创建 PriorityBlockingQueue 时要传入一个 Comparator,在比较规则下越小的越优先。
DelayQueue 延期队列
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue<E>();
private Thread leader;
private final Condition available = lock.newCondition();
Delayed 接口
DelayQueue 内部持有一个 PriorityQueue,表明延迟队列也是一个支持优先级的队列。只不过延迟队列的优先级是 到期时间 。
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
Delayed 接口定义的 getDelay 方法返回的就是剩余延期时间,返回值 > 0,还没到期,返回值 ≤ 0 表示 已到期。
PriorityQueue 优先级比较的就是这个剩余延期时间,排在二叉堆顶的永远是最近到期的元素。Delayed 元素示例如下:
class DelayedTask implements Delayed {
private final long expireTime; // 到期时刻(纳秒)
private final String taskName;
public DelayedTask(long delay, TimeUnit unit, String taskName) {
this.expireTime = System.nanoTime() + unit.toNanos(delay);
this.taskName = taskName;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(expireTime - System.nanoTime(), NANOSECONDS);
}
@Override
public int compareTo(Delayed other) {
return Long.compare(this.getDelay(NANOSECONDS), other.getDelay(NANOSECONDS));
}
@Override
public String toString() {
return taskName + " (剩余 " + getDelay(TimeUnit.SECONDS) + " 秒)";
}
}
compareTo 和 getDelay 的逻辑必须一致——排序依据就是剩余延迟时间,否则堆顶可能不是真正最近到期的元素,DelayQueue 的行为就会错乱。
普通的队列,阻塞行为体现在队列为空时 take ,队列满时 put,而这个 DelayQueue 的阻塞行为就是 take 的时候,有没有到期元素,只有堆顶元素 getDelay() <= 0 时,才会真正返回。
所以 DelayQueue 也只有一个 Condition available,用来表达 现在有没有“已到期、可被取走”的元素。
Leader-Follower 模式
如果队列未到期,则 available.awaitNanos(delay) 。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 获取可中断锁,避免线程在等待期间无法响应中断
lock.lockInterruptibly();
try {
// 自旋等待,直到成功取出一个已到期元素
for (;;) {
// 查看堆顶元素(最近到期的那个),但不移除
E first = q.peek();
if (first == null) {
// 队列为空,没有元素可消费,无限等待直到有元素入队
available.await();
} else {
// 获取堆顶元素的剩余延迟时间(纳秒)
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0L) {
// 已到期,直接出队并返回
return q.poll();
}
// 还没到期,断开引用,避免在等待期间持有该对象导致 GC 无法回收
first = null;
if (leader != null) {
// 已有其他线程担任 leader(正在精确等待到期时间),
// 当前线程作为 follower 无限等待,由 leader 到期出队后唤醒
available.await();
} else {
// 当前没有 leader,本线程成为 leader,负责精确等待到期时间
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 精确等待剩余延迟时间,到期后自动唤醒
available.awaitNanos(delay);
} finally {
// 如果本线程仍然是 leader(说明没有被其他线程抢占),则释放 leader 身份
if (leader == thisThread) {
leader = null;
}
}
}
}
}
} finally {
// 如果 leader 已释放且队列中仍有元素,唤醒下一个等待线程接替成为新 leader
if (leader == null && q.peek() != null) {
available.signal();
}
// 释放锁
lock.unlock();
}
}
这个 Leader-Follower 模式是 DelayQueue 最精彩的设计:
- Leader:只有一个线程担任,它知道堆顶元素的到期时间,所以用
awaitNanos(delay)精确等待。 - Follower:其余线程不知道要等多久,干脆
await()无限等待,等 leader 到期出队后唤醒它们。
所有说 take 阻塞的线程不仅仅是由 offer/put 的线程唤醒:
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e);
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
offer 只在 新入队的元素到期时间比队列中元素的到期时间都短的时候,也就是 e 成了堆顶时,才唤醒,因为当前 leader 可能正在等一个更晚到期的元素,如果不通知它,就会白白等到旧堆顶到期。
这也是它只需要一个 Condition 就能同时处理“空队列等待”和“未到期等待”的原因——两种情况最终都归结为同一个问题:现在有没有可消费的元素。
TransferQueue 传递队列
通过 UML 类图,可以看出有一个 TransferQueue 接口拓展了 BlockingQueue 。
TransferQueue 字面意思是传递队列,"传递"强调的是元素从生产者线程直接交到消费者线程这件事,而不是"放进队列里存着"。
它新增了四个方法:
| 操作 | 方法 | 说明 |
|---|---|---|
| 转交 | void transfer(E e) | 若当前存在一个正在等待获取的消费者线程,即立刻移交之;否则,会插入当前元素 e 到队列尾部,并且等待进入阻塞状态,直到有消费者线程取走该元素 |
| 非阻塞尝试转交 | boolean tryTransfer(E e) | 有若当前存在一个正在等待获取的消费者线程(使用 take() 或 poll()),会即刻转移/传输对象元素 e;若不存在,则返回 false,并且不进入队列,不阻塞、不入队 |
| 带超时尝试转交 | boolean tryTransfer(E e, long timeout, TimeUnit unit) | 若当前存在一个正在等待获取的消费者线程,会立即传输给它;否则将插入元素 e 到队列尾部,并等待被消费者线程获取消费掉;若指定时间内元素 e 无法被消费者获取,则返回 false,同时该元素被移除 |
| 判断 | boolean hasWaitingConsumer() / int getWaitingConsumerCount() | 判断 / 获取当前阻塞等待的消费者数量。 |
对于 BlockingQueue 来说,生产者只需要队列有空位,元素能放进队列就行,放完立刻返回,不关心元素什么时候被消费。
而 TransferQueue 可以让生产者阻塞,直到元素被消费者拿走才返回。相当于生产者要等到 “交付完成”。
下面直接看它的唯一的实现类 LinkedTransferQueue。
LinkedTransferQueue 链表传递队列
public class LinkedTransferQueue<E> extends AbstractQueue<E>
implements TransferQueue<E>, java.io.Serializable {
static final class DualNode implements ForkJoinPool.ManagedBlocker {
volatile Object item; // initially non-null if isData; CASed to match
DualNode next; // accessed only in chains of volatile ops
Thread waiter; // access order constrained by context
final boolean isData; // false if this is a request node
DualNode(Object item, boolean isData) {
ITEM.set(this, item); // relaxed write before publication
this.isData = isData;
}
}
transient volatile DualNode head;
transient volatile DualNode tail;
transient volatile int sweepVotes;
虽然都是基于链表实现的队列,但 LinkedTransferQueue 的节点和 LinkedBlockingQueue 有点不一样。
链表传递队列的节点叫 DualNode 双重节点,是“双重”,不是“双向”,是因为这个节点一个顶俩,它既代表数据,又代表请求。
- 数据节点(生产者节点) :
isData = true,生产者调用transfer/put创建。item存放要交付的元素。 - 请求节点(消费者节点) :
isData = false,消费者调用take/poll创建。item = null,代表消费者在等待拿数据。
DualNode 里的 Thread 就是阻塞等待的线程。下面看一下这个 transfer 方法,深刻理解一下:
public void put(E e) {
Objects.requireNonNull(e);
xfer(e, -1L);
}
public boolean offer(E e, long timeout, TimeUnit unit) {
Objects.requireNonNull(e);
xfer(e, -1L);
return true;
}
public boolean add(E e) {
Objects.requireNonNull(e);
xfer(e, -1L);
return true;
}
public boolean tryTransfer(E e) {
Objects.requireNonNull(e);
return xfer(e, 0L) == null;
}
public void transfer(E e) throws InterruptedException {
Objects.requireNonNull(e);
if (!Thread.interrupted()) {
if (xfer(e, Long.MAX_VALUE) == null)
return;
Thread.interrupted(); // failure possible only due to interrupt
}
throw new InterruptedException();
}
无论是 add、offer、put、transfer 等所有阻塞与非阻塞操作,底层都是统一的 xfer 方法实现的。
其实不止新增方法,移除方法(poll、take)底层同样是 xfer 方法:
/**
* @param e 元素:生产者传入元素,消费者传入null
* @param ns 超时纳秒;0=不阻塞;负数=无限阻塞;正数=超时等待
* @return 匹配成功返回元素;失败/超时/中断返回e
*/
final Object xfer(Object e, long ns) {
boolean haveData = (e != null); // true=生产者(数据节点,带元素);false=消费者(请求节点,e=null)
Object m; // 匹配结果:匹配成功拿到的元素;匹配失败等于e
DualNode s = null, p; // s: 当前线程新建待入队节点;p: 遍历链表时的当前节点
// restart标签:CAS竞争失败、链表结构变化时,回到外层循环重新从头执行
restart: for (DualNode prevp = null;;) {
DualNode h, t, q;
// 分支:队列为空(head==null)
if ((h = head) == null &&
(ns == 0L || // 非阻塞模式,队列空直接放弃,不初始化队列
// CAS尝试把新节点s设置成head,完成队列初始化;CAS失败说明并发竞争,进入下一轮循环
(h = cmpExHead(null, s = new DualNode(e, haveData))) == null)) {
p = null;
break; // 队列初始化成功,跳出循环,准备阻塞等待匹配
}
// 选取遍历起点p:优先从tail开始(tail节点类型和当前节点同类型,直接追加尾部);否则从head开始遍历
p = (t = tail) != null && t.isData == haveData && t != prevp ? t : h;
prevp = p; // 记录上一轮起点,防止循环卡在自链接的废弃节点
// 内层循环:从p开始遍历链表节点,尝试匹配互补节点
do {
m = p.item;
q = p.next; // q是p的后继节点
// ========== 条件:找到了【互补类型节点】,可以尝试CAS匹配交付 ==========
// p.isData != haveData:节点类型相反(生产者遇到消费者节点,消费者遇到生产者节点)
// haveData != (m != null):对方节点当前还未完成匹配(item状态空闲)
// p.cmpExItem(m, e) == m:CAS替换item成功,完成元素交付
if (p.isData != haveData && haveData != (m != null) &&
p.cmpExItem(m, e) == m) {
Thread w = p.waiter; // 获取阻塞在这个互补节点上的等待线程
// 匹配成功,尝试推进head指针:把匹配完的旧节点移出队列,head前移
if (p != h && h == cmpExHead(h, (q == null) ? p : q))
h.next = h; // 旧head自链接,标记为废弃节点,后续惰性清理
LockSupport.unpark(w); // 唤醒对方阻塞的线程
return m; // 返回匹配结果,方法结束
}
// ========== 条件:遍历到链表尾部(q == null),没有找到可匹配节点 ==========
else if (q == null) {
if (ns == 0L) // 非阻塞模式,不创建节点不入队,直接重启循环返回
break restart;
if (s == null) // 阻塞模式,第一次走到尾部,新建当前线程的节点
s = new DualNode(e, haveData);
// CAS尝试把当前节点s追加到p的next(链表尾部)
if ((q = p.cmpExNext(null, s)) == null) {
// CAS入队成功;如果p不是tail,尝试CAS更新tail指向新节点s(并发下可能失败,无所谓)
if (p != t)
cmpExTail(t, s);
break restart; // 入队完成,跳出内层循环,准备阻塞等待匹配
}
// CAS失败:说明并发下其他线程修改了p.next,q拿到新的后继节点,继续内层循环遍历
}
} while (p != (p = q)); // 遍历推进:p = q;如果p == q代表节点自链接(废弃节点),终止内层循环
}
// ========== 走到这里:节点s已经成功入队 ==========
if (s == null || ns <= 0L)
m = e; // 没有创建节点 / 非阻塞:不park,直接返回e
else
// 阻塞等待:调用await自旋+park,等待被匹配、中断或者超时
// 返回m:匹配成功拿到元素;超时/中断返回传入的e
if ((m = s.await(e, ns, this,
p == null || p.waiter == null)) == e)
unsplice(p, s); // 超时/中断:节点取消,惰性尝试把s从链表摘除
else if (m != null)
s.selfLinkItem(); // 匹配成功,标记节点item自链接,标记废弃
return m;
}
}
这段代码的核心逻辑就是:
- 遍历链表,尝试找互补类型节点进行CAS匹配交付;
- 找到互补节点 → CAS完成元素交付,唤醒对方线程,直接返回;
- 找不到互补节点:对于非阻塞模式(ns=0),直接返回,不入队;对于阻塞模式,新建 DualNode 追加到链表尾部,线程park阻塞等待被匹配;
整个 xfer 没有synchronized/ReentrantLock,并发控制靠一系列 CAS:cmpExHead,CAS 更新 head;cmpExTail,CAS 更新 tail;
node.cmpExItem,CAS 修改节点 item,完成元素交付;node.cmpExNext:CAS 修改 next 指针,追加节点。
生产-消费匹配用的就是 casItem:
// 尝试把节点的item,从旧值item改成e
p.casItem(item, e) // 生产者节点 `item`存数据;消费者节点 `item=null`,代表等待拿数据
当发现队列头部是相反类型节点(生产者遇到等待消费者 / 消费者遇到等待生产者),执行 casItem,原子修改对方节点的 item,完成数据配对交付。
配对完成,不需要入队;失败说明被别的线程抢先匹配,继续循环重试。
CAS 失败不会自旋死等,直接跳到restart标签外层循环重试,重新遍历链表。
通过 UML 还可以看到 SynchronousQueue 有个内部类 Transferer 继承了这个 链表传递队列,那 SynchronousQueue 又是什么呢?
SynchronousQueue 同步队列
可以把 SynchronousQueue 理解成容量为 0 的链表传递队列,
public boolean isEmpty() {
return true;
}
public int size() {
return 0;
}
public int remainingCapacity() {
return 0;
}
public E peek() {
return null;
}
SynchronousQueue 的容量为 0,没有一个地方来暂存元素,或者一些涉及容量的方法都被重写了,isEmpty() 始终返回 true,size() 始终返回 0,remainingCapacity() 始终返回 0,contains() 返回 false。
SynchronousQueue 只做线程间的“手递手交付”,put 和 take 必须配对,一方阻塞直到另一方到来。
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
if (!Thread.interrupted()) {
if (xfer(e, Long.MAX_VALUE) == null)
return;
Thread.interrupted(); // failure possible only due to interrupt
}
throw new InterruptedException();
}
public E take() throws InterruptedException {
Object e;
if (!Thread.interrupted()) {
if ((e = xfer(null, Long.MAX_VALUE)) != null)
return (E) e;
Thread.interrupted();
}
throw new InterruptedException();
}
/**
* SynchronousQueue 的顶层转发方法
* 所有 put/take/offer/poll 最终都会调用这个 xfer
* @param e 元素:生产者传入非null;消费者传入null
* @param nanos 超时纳秒;0=不阻塞;负数=永久阻塞;正数=限时阻塞
* @return 成功:消费者返回拿到的元素;生产者返回null;失败/中断/超时返回e
*/
private Object xfer(Object e, long nanos) {
// transferer 是核心策略对象,JDK初始化时根据fair选择实现类
Transferer<E> x = transferer;
// fair=true:公平模式 TransfererQueue,调用FIFO版本 xfer()
// fair=false:非公平模式 TransfererStack,调用LIFO版本 xferLifo()
return (fair) ? x.xfer(e, nanos) : x.xferLifo(e, nanos);
}
x.xfer 就是调用 链表传递队列的 xfer 方法,对应非公平模式对应公平模式,x.xferLifo 就是内部类自己定义的方法,对应非公平模式。
SynchronousQueue 虽然不存储元素,但没有元素不代表没有节点,它是要创建代表等待线程的节点。
假设有 t1、t2、t3 三个线程先后 put 并阻塞,此时 t4 来 take,那到底哪个 put 被配对?
如果是公平模式,那肯定是先进先出,t1 放的那个元素被取出;那非公平模式,后进先出,t3 放的那个元素被取出。
总结
自此,七大核心阻塞队列梳理完毕。总的来说,它们作为阻塞队列,都具备相同的阻塞语义:队空时,获取元素的线程阻塞;队满时,放入元素的线程阻塞。
其中LinkedTransferQueue比较特殊,没有使用 ReentrantLock,依靠 CAS + LockSupport.park/unpark 完成并发控制与线程阻塞唤醒。其余阻塞队列,存取操作都基于 ReentrantLock 来实现线程互斥与条件等待。
下面来个小总结:
| 队列 | 存储结构 | 有界 / 无界 | 锁模型 | 核心特点 | 典型场景 |
|---|---|---|---|---|---|
| ArrayBlockingQueue | 数组 | 有界,创建指定容量 | 单 ReentrantLock 一把锁,notEmpty、notFull 两个条件 | 数组,先进先出;生产消费互斥;支持公平 / 非公平锁;不能扩容 | 固定容量限流,削峰;简单稳定,任务队列 |
| LinkedBlockingQueue | 双向链表 | 可配置有界,默认无界 | 双锁(读锁、写锁) ,锁分离 | FIFO;生产消费可并发;size 精确;尾插头取 | 普通高吞吐生产者消费者 |
| LinkedBlockingDeque | 单向链表 | 可配置有界,默认无界 | 一把 ReentrantLock | 双端队列,putFirst/takeLast;支持 FIFO/LIFO | 工作窃取、任务栈、头部插队 |
| PriorityBlockingQueue | 平衡二叉堆(数组实现堆) | 无界 | 一把 ReentrantLock | 元素按优先级排序,不是 FIFO;最小堆;没有容量上限;支持自定义 Comparator;无界会 OOM | 任务按优先级调度(如订单优先级、告警优先级) |
| DelayQueue | PriorityQueue(优先队列) | 无界 | 一把 ReentrantLock+Condition | 元素必须实现 Delayed 接口,到期才能取出;底层包装 PriorityBlockingQueue | 定时任务、订单超时、缓存过期、延迟重试 |
| LinkedTransferQueue | DualNode 单向 volatile 链表 | 无界 | 无锁,全 CAS,DualNode 双类型节点 | 优先直接交付;匹配失败可入队缓冲;transfer 阻塞直到被消费;DualNode 支持 ManagedBlocker;size () 开销大 | 高并发消息交付;需要确认消费;ForkJoin 内部线程数据传递 |
| SynchronousQueue | 链表节点 | 容量 = 0,不存元素 | 无锁 CAS | 纯手递手,必须生产者消费者配对;公平 / 非公平;队列只存等待线程,不存数据 | 零缓冲一对一同步交付 |
附:UML类图
阻塞队列
@startuml
interface Collection<E>{}
abstract class AbstractCollection<E> implements Collection{}
interface Queue<E> extends Collection{}
abstract class AbstractQueue<E> extends AbstractCollection implements Queue{}
interface BlockingQueue<E> extends Queue{}
class ArrayBlockingQueue<E> extends AbstractQueue implements BlockingQueue{}
class LinkedBlockingQueue<E> extends AbstractQueue implements BlockingQueue{}
class PriorityBlockingQueue<E> extends AbstractQueue implements BlockingQueue{}
class DelayQueue<E extends Delayed> extends AbstractQueue implements BlockingQueue{}
interface TransferQueue<E> extends BlockingQueue{}
@enduml
双端队列
@startuml
interface Deque<E> extends Queue{}
interface BlockingDeque<E> extends BlockingQueue, Deque{}
abstract class AbstractQueue<E> extends AbstractCollection implements Queue{}
class LinkedBlockingDeque extends AbstractQueue implements BlockingDeque{}
@enduml
传递队列
@startuml
interface TransferQueue<E> extends BlockingQueue{}
class LinkedTransferQueue<E> extends AbstractQueue implements TransferQueue{}
class SynchronousQueue$Transferer <E> extends LinkedTransferQueue{}
class SynchronousQueue<E> extends AbstractQueue implements BlockingQueue{}
@enduml