聊聊jdk中的阻塞队列

196 阅读6分钟

引言

在使用线程池 executor 时,有一个参数是阻塞队列 BlockingQueue。什么是阻塞队列呢?阻塞队列就是实现了多线程下生产者-消费者模型的一个东西。生产者将“物品”存入队列,如果队列已满则一直阻塞,直至队列有空闲;消费者从队列中取出“物品”,如果队列为空就一直阻塞,直至队列不为空。对于线程池来说,提交的“任务”就是所谓的物品。

运用阻塞队列的特性,线程池实现了不同的任务提交策略。比如 Executors 中的 newFixThreadPool 和 newSingleThreadExecutor 就是使用了阻塞队列 LinkedBlockingQueue。具体的来说,在executor()方法中,调用了阻塞队列的offer(E e)方法来尝试将任务入队;而工作线程则通过poll(long timeout, TimeUnit unit)或者take()方法来获取要执行的任务。

poll()take()同样是获取队列中的任务,但是行为却不同:如果队列中没有任务,poll()会直接返回 false,而 take()则会一直阻塞,直至有任务可以被取到。BlockingQueue 中一共定义了四组放入、取出的方法,分别如下:

动作抛异常返回值阻塞超时
放入add(e)offer(e)put(e)offer(e, time, unit)
取出remove()poll()take()poll(time, unit)

jdk 中的阻塞队列一共 7 个,分别是 ArrayBlockingQueue、LinkedBlockingQueue、LinkedBlockingDeque、PriorityBlockingQueue、DelayQueue、SynchronousQueue、LinkedTransferQueue。

下面就简单分析一下它们。

ArrayBlockingQueue

首先是构造方法:

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0) // 初始容量
        throw new IllegalArgumentException();
    this.items = new Object[capacity]; // 排队对象
    lock = new ReentrantLock(fair); // 可重入锁
    notEmpty = lock.newCondition(); // condition
    notFull =  lock.newCondition(); // condition
}

从构造方法里,可以看到 ArrayBlockingQueue 的成员变量有一个可重入锁 lock,以及两个 condition。

lock 主要控制这几个变量的共享:

final Object[] items; // 元素集合

int takeIndex; // 下一个取出元素的下标

int putIndex; // 下一个添加元素的下标

int count; // 元素数量

items 和 count 变量无需解释了,而 takeIndex 和 putIndex 该如何理解呢?这里可以先看一下 put()方法是如何实现的:

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) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x; // 设置下标为putIndex的元素
    if (++putIndex == items.length) // 从头循环
        putIndex = 0;
    count++;
    notEmpty.signal(); // 唤醒notEmpty
}

由此可见,ArrayBlockingQueue 实际上循环利用了底层的数组。先从头至尾从数组中移除元素,当添加至末尾时,将会从头开始继续添加。而具体的数组有没有满,则是通过 countitems.length来判断的。take()方法也是如此,非常类似,这里就不在多述了:

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() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal();
    return x;
}

可以看到,put(e)take()利用notFullnotEmpty两个 Condition 实现了放入取出的阻塞,那么立即获取返回值的offer(e)poll()方法是如何实现的呢?实际上非常简单,只要判断countitems.length的大小关系就可以。如果count==items.length,那么队列已满,直接返回 false 就可以:

public boolean offer(E e) {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        if (count == items.length) // 队列已满,直接返回false
            return false;
        else {
            enqueue(e);
            return true;
        }
    } finally {
        lock.unlock();
    }
}

LinkedBlockingQueue

照样先来看构造方法:

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); // 头、尾指针
}

可以看到初始化 LinkedBlockingQueue 时必须传入一个 capacity、也就是容量。所以说 LinkBlockingQueue 是一个无界队列其实是不准确的,它是一个必须指定容量的有界队列,只不过默认的空参数构造方法传入的容量是Integer.MAX_VALUE,所以会看似是无界的。这也是会导致内存溢出的原因。

和 ArrayBlockingQueue 类似,LinkedBlockingQueue 也是通过锁、condition 和count来控制队列的并发和阻塞。但与 ArrayBlockingQueue 不同的是,LinkedBlockingQueue 控制的粒度更加小,不是直接通过一个粗粒度的 ReentrantLock 控制所有贡献变量的访问,而是通过了两把锁和原子类 AtomicInteger:

private final AtomicInteger count = new AtomicInteger(); // 元素计数

private final ReentrantLock takeLock = new ReentrantLock(); // 取出元素的锁

private final Condition notEmpty = takeLock.newCondition(); // 取出元素的condition

private final ReentrantLock putLock = new ReentrantLock(); // 放入元素的锁

private final Condition notFull = putLock.newCondition(); // 放入元素的condition

因为取出和放入使用了不用的锁,所以对于 LinkedBlockingQueue 而言,生产和消费两者完全独立,可以同时进行而不像 ArrayBlockingQueue 一样,会有一端停下来等待。那为什么 ArrayBlockingQueue 不用两把锁呢?因为 ArrayBlockingQueue 底层循环利用了数组,为了保证数组的一致性,必须使用同一把锁。

LinkedBlockingDeque

LinkedBlockingDeque 和 LinkedBlockingQueue 类似,底层都是使用链表结构来存储元素。但不同的是,LinkedBlockingQueue 是一个单向链表,每个结点只存储了 next 引用,而 LinkedBlockingDeque 是双向链表,除了 next 引用外还存储了 prev、也就是上一个结点的引用。对比单向队列 Queue,双端队列 Deque 支持从队列头或者队列尾增加和删除元素,如addFirst(E)addLast(e)removeFirst()removeLast()等方法。

在并发控制上,LinkedBlockingDeque 和 ArrayBlockingQueue 一样,通过全局的一把锁、count 以及 notEmpty、notFull 两个 condition 对象来控制阻塞,这里就不再粘贴代码了。注意到 LinkedBlockingDeque 也是有容量的。

PriorityBlockingQueue

优先堵塞队列是一个无界的阻塞队列。虽然没有最大容量的限制,但是在初始化的时候,可以指定初始容量,在容量不足的时候可以自动扩容。底层是用数组表示的平衡二叉堆,根据自然排序(实现 compareble 接口)或者比较器(comparator 类)排序。同样用一把可重入锁来实现并发,但由于是无界的,所以只有一个 notEmpty 的 condition 对象来控制take()的阻塞,至于put()方法则是永远不会阻塞的。

public void put(E e) {
    offer(e); // 直接调用offer()方法,永远不阻塞
}

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    E result;
    try {
        while ( (result = dequeue()) == null)
            notEmpty.await(); // 空队列阻塞
    } finally {
        lock.unlock();
    }
    return result;
}

DelayQueue

延时队列 DelayQueue 也是个无界队列,底层是用优先队列 PriorityQueue 实现的。DelayQueue 队列的元素需要实现Delayed接口的getDelay()方法。只有该方法的返回值小于等于 0,才能出列。因为是无界队列,所以put()方法也是直接调用了offer(),并不会产生阻塞:

public void put(E e) {
    offer(e);
}

public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock(); // 加锁
    try {
        q.offer(e); // 进优先队列 PriorityQueue
        if (q.peek() == e) { // 添加元素在头部
            leader = null;  // 因为是新头部,所以等待它的线程设为null
            available.signal(); // 通知等待线程
        }
        return true;
    } finally {
        lock.unlock(); // 解锁
    }
}
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 <= 0) // 延迟时间到,返回元素
                    return q.poll();
                first = null;
                if (leader != null) // 有其他的线程等待头结点,则本线程一直等待
                    available.await();
                else {
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        available.awaitNanos(delay); // 本线程限时等待头结点
                    } finally {
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && q.peek() != null)
            available.signal();
        lock.unlock();
    }
}

SynchronousQueue

SynchronousQueue 是一个非常特殊的队列,它没有容量的概念,当一个线程往里放数据的时候,必须有另一个线程从中取走数据;反之亦然,当一个线程从中取数据的时候,必须有另一个线程往里面放数据。换句话说,就是只有放入与取出操作“配对”,才能成功进行。在线程池中,Executors.newCachedThreadPool()就使用了这个队列。

构造方法如下:

public SynchronousQueue(boolean fair) {
    transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}

从构造方法我们可以知道,SynchronousQueue 内部有个transferer变量。这个变量是对Transferer接口对象的一个引用。通过Transferer对象的不同实现(队列或者堆栈),可以实现公平模式或者非公平模式。同时,这个对象也是实现“配对”操作的关键。

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    if (transferer.transfer(e, false, 0) == null) {
        Thread.interrupted();
        throw new InterruptedException();
    }
}

public E take() throws InterruptedException {
    E e = transferer.transfer(null, false, 0);
    if (e != null)
        return e;
    Thread.interrupted();
    throw new InterruptedException();
}

可以看到put()take()主要调用了transferer.transfer(E e,boolean timed,long nanos)方法。当 e 为 null 时,当作消费者处理;当 e 不为 null 时,当作生产者处理。Transferer有两个实现,一个是保证公平性的TransferQueue,另一个是非公平的TransferStack。默认是非公平性的。

这里主要看看TransferQueuetransfer实现:

E transfer(E e, boolean timed, long nanos) {
    QNode s = null;
    boolean isData = (e != null); // true:放入操作;false:取出操作

    for (;;) {
        QNode t = tail;
        QNode h = head; // head本身是一个dummy结点,head.next指向真实的第一个结点
        if (t == null || h == null)
            continue;

        if (h == t || t.isData == isData) { // 队列为空,或者操作相同(都是放入或者取出),则入队
            QNode tn = t.next;
            if (t != tail) // tail被另一个线程改变了
                continue;
            if (tn != null) { // tail.next被另一个线程改过了,追加到新tail后面
                advanceTail(t, tn);
                continue;
            }
            if (timed && nanos <= 0) // 限时等待超时,直接返回null
                return null;
            if (s == null) // 构建新结点s
                s = new QNode(e, isData);
            if (!t.casNext(null, s)) // 如果设置tail.next失败,则循环cas,直至成功
                continue;
            // 执行到这里,说明已经成功设置新的尾部了
            advanceTail(t, s); // 设置新的tail
            Object x = awaitFulfill(s, e, timed, nanos); // 等待(先自旋后挂起),如果被中断或者限时等待超时则返回s,否则返回配对的对象
            if (x == s) { //被取消或者超时
                clean(t, s);
                return null;
            }
            // 执行到这里,说明已经成功配对了
            if (!s.isOffList()) { // 尚未取消连接
                advanceHead(t, s);          // unlink if head
                if (x != null)              // and forget fields
                    s.item = s;
                s.waiter = null;
            }
            // 配对对象不为null,说明对方是生产者,返回对方生产的数据;配对对象为null,说明对方是消费者,自己是生产者,返回自己生产的数据
            return (x != null) ? (E)x : e;

        } else { // 操作互补
            QNode m = h.next;
            if (t != tail || m == null || h != head) // 被另一个线程改变了
                continue;

            Object x = m.item;
            if (isData == (x != null) ||    // 无法匹配m,说明m已经成功匹配
                x == m ||                   // m被取消
                !m.casItem(x, e)) {         // CAS设置配对的item失败
                advanceHead(h, m);          // m出队,重试
                continue;
            }

            advanceHead(h, m); // 配对完成,出队
            LockSupport.unpark(m.waiter);
            return (x != null) ? (E)x : e;
        }
    }
}

整体来说,是使用一个队列新的任务排队。如果队列为空或者新任务的动作和 tail 结点相同(都是 add/offer/put 或者 remove/poll/take),将会追加到队列尾,并进行等待(先自旋后挂起)。反之,则说明操作互补,那么就会从头开始匹配。这里因为 head 是一个哨兵结点,所以从 head.next 开始尝试配对。如果互补配对成功则出队,反之继续重试。

LinkedTransferQueue

LinkedTransferQueue 是个无界的阻塞队列,实现了 BlockingQueue 和子接口 TransferQueue。TransferQueue 主要描述一个生产者可能会等待消费者取走元素的队列。相对于 BlockingQueue,TransferQueue 增加了这些方法:

// 尝试放入元素,如果有消费者等待则立即放入并返回true,否则返回false
boolean tryTransfer(E e);

// 尝试放入元素,如果没有消费者则等待
void transfer(E e) throws InterruptedException;

// 尝试在一定时间内放入元素
boolean tryTransfer(E e, long timeout, TimeUnit unit)
    throws InterruptedException;

// 是否有等待的消费者
boolean hasWaitingConsumer();

// 等待的消费者数量
int getWaitingConsumerCount();

类似于 SynchronousQueue,LinkedTransferQueue 使用了一个xfer(E e, boolean haveData, int how, long nanos)方法实现 put、take 和 transfer 的相关方法:


public void put(E e) {
    xfer(e, true, ASYNC, 0);
}

public E take() throws InterruptedException {
    E e = xfer(null, false, SYNC, 0);
    if (e != null)
        return e;
    Thread.interrupted();
    throw new InterruptedException();
}

public boolean tryTransfer(E e) {
    return xfer(e, true, NOW, 0) == null;
}

下面就来分析一下这个xfer:

private E xfer(E e, boolean haveData, int how, long nanos) {
    if (haveData && (e == null))
        throw new NullPointerException();
    Node s = null;                        // the node to append, if needed

    retry:
    for (;;) {                            // restart on append race
        for (Node h = head, p = h; p != null;) { // find & match first node
            boolean isData = p.isData; // true是生产者,false是消费者
            Object item = p.item;
            // item==p说明被取消,item==null&&isData==false说明是个消费结点,item!=null&&isData==true说明是个生产结点,这两种结点都没有匹配成功
            if (item != p && (item != null) == isData) { // unmatched
                if (isData == haveData)   // can't match
                    break;
                if (p.casItem(item, e)) { // match
                    for (Node q = p; q != h;) {
                        Node n = q.next;  // update by 2 unless singleton
                        if (head == h && casHead(h, n == null ? q : n)) {
                            h.forgetNext();
                            break;
                        }                 // advance and retry
                        if ((h = head)   == null ||
                            (q = h.next) == null || !q.isMatched())
                            break;        // unless slack < 2
                    }
                    LockSupport.unpark(p.waiter);
                    return LinkedTransferQueue.<E>cast(item);
                }
            }
            Node n = p.next;
            p = (p != n) ? n : (h = head); // Use head if p offlist
        }

        if (how != NOW) {                 // No matches available
            if (s == null)
                s = new Node(e, haveData);
            Node pred = tryAppend(s, haveData);
            if (pred == null)
                continue retry;           // lost race vs opposite mode
            if (how != ASYNC) // add,offer,put时值为ASYNC,所以生产时不堵塞
                return awaitMatch(s, pred, e, (how == TIMED), nanos);
        }
        return e; // not waiting
    }
}

方法主要通过haveData字段判断是生产者还是消费者。每当放入或者取出结点 e 的时候,遍历队列,如果发现遍历的结点 e1 和 e 互补配对(一个生产者,一个消费者),那么就返回匹配的结点;如果没找到匹配的结点,那么就把该结点入队。