阻塞队列 BlockingQueue

5 阅读23分钟

前边介绍了 juc 的锁体系 —— ReentrantLock、 ReentrantReadWriteLock 和 StampedLock。

有了锁,从本篇文章开始,开始介绍锁的应用,本篇文章先讨论一种并发容器 —— BlockingQueue。

BlockingQueue :是线程安全的队列,顾名思义,它是个阻塞队列,队列满时,入队线程阻塞;队列空时,出队线程阻塞,常用于生产者-消费者模型。

UML 类图

image.png

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 基于链表的双向阻塞队列

image.png

从 UIM 类图上,可以看出这个双向队列实现的是 Deque、BlockingDeque。

头尾操作

Deque 全称Double Ended Queue,表示双端队列,普通 Queue 只能尾部入队、头部出队,FIFO 先进先出;而 双端队列 头部、尾部两端都可以插入和删除元素。

而 BlockingDeque 又实现了 BlockingQueue,说明它继承阻塞语义,队空阻塞取元素、队满阻塞放元素,所以它天然同时拥有双端操作 + 阻塞等待两套能力。

操作抛异常返回特殊值永久阻塞超时阻塞
队头插入addFirstofferFirstputFirstofferFirst(e,time,unit)
队尾插入addLastofferLastputLastofferLast(e,time,unit)
队头取出并删除removeFirstpollFirsttakeFirstpollFirst(time,unit)
队尾取出并删除removeLastpollLasttakeLastpollLast(time,unit)
队头查看 (不删)getFirstpeekFirst——
队尾查看 (不删)getLastpeekLast——

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 。

image.png

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;
}

}

这段代码的核心逻辑就是:

  1. 遍历链表,尝试找互补类型节点进行CAS匹配交付;
  2. 找到互补节点 → CAS完成元素交付,唤醒对方线程,直接返回;
  3. 找不到互补节点:对于非阻塞模式(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任务按优先级调度(如订单优先级、告警优先级)
DelayQueuePriorityQueue(优先队列)无界一把 ReentrantLock+Condition元素必须实现 Delayed 接口,到期才能取出;底层包装 PriorityBlockingQueue定时任务、订单超时、缓存过期、延迟重试
LinkedTransferQueueDualNode 单向 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