Java中的阻塞队列BlockingQueue

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。

  • 支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满,如 put() 方法。
  • 支持阻塞的移除方法:在队列为空时,获取元素的线程会等待队列变为非空,如 take() 方法。

BlockingQueue 常用于生产者-消费者场景,生产者是往队列里添加元素的线程,消费者是从队列里取元素的线程。BlockingQueue就是存放元素的容器

阻塞队列提供了四组不同的方法用于插入、移除、检查元素:

方法/处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
检查方法 element() peek() - -
  • 抛出异常:如果试图的操作无法立即执行则抛出异常。当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException 异常 。
  • 返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回 true。如果是移除方法,队列为空时返回 null。
  • 一直阻塞:如果试图的操作无法立即执行,则一直阻塞或者响应中断。
  • 超时退出:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功,通常是 true / false。

BlockingQueue 的实现类

以下是 7 种阻塞队列:

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列
  • LinkedBlockingQueue:由链表结构组成的阻塞队列(可无界、可有界)
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列
  • DelayQueue:使用优先级队列实现的无界阻塞队列
  • SynchronousQueue:不存储元素的阻塞队列
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列

FIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)

优先级队列 :PriorityBlockingQueue


ArrayBlockingQueue

ArrayBlockingQueue 是一个用数组实现的有界阻塞队列。按照先进先出(FIFO)的原则对元素进行排序。

可以初始化队列大小, 且一旦初始化,容量不能改变。构造方法中的 fair 表示控制对象的内部锁是否采用公平锁,默认是非公平锁。并发控制以及访问者的公平性是使用可重入锁 ReentrantLock 实现的

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 默认情况下不保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。而非公平性则是指访问的顺序不是遵守严格的时间顺序,有可能导致饥饿现象。如果保证公平性,通常会降低吞吐量。

final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;

ArrayBlockingQueue 实现并发同步的原理就是,读操作和写操作都需要获取到 AQS 独占锁才能进行操作。如果队列为空,这个时候读操作的线程进入到读线程队列排队,等待写线程写入新的元素,然后唤醒读线程队列的第一个等待线程。如果队列已满,这个时候写操作的线程进入到写线程队列排队,等待读线程将队列元素移除腾出空间,然后唤醒写线程队列的第一个等待线程。


LinkedBlockingQueue

LinkedBlockingQueue 底层是基于单链表实现的阻塞队列,可以是无界队列也可以是有界队列。可以通过构造方法指定容量大小来创建有界队列,也可以不指定容量大小,默认队列的大小是Integer.MAX_VALUE,此时创建的就是无界队列。我们可以通过查看源码来验证:

static class Node<E> {
    E item;
   	// 只有后驱指针,说明是单链表
    Node<E> next;
    Node(E x) { item = x; }
}

// 容量限制,如果没有设置,则为 Integer.MAX_VALUE
private final int capacity;

/**
 * 无界队列
 */
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 实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

// 由 take、poll 等持有的锁
private final ReentrantLock takeLock = new ReentrantLock();
// 由 put、offer 等持有的锁
private final ReentrantLock putLock = new ReentrantLock();

Java中的阻塞队列BlockingQueue_第1张图片

图源:https://javadoop.com/post/java-concurrent-queue


PriorityBlockingQueue

PriorityBlockingQueue 是一个支持优先级的无界阻塞队列,底层是基于数组的二叉堆实现的,默认情况下元素采用自然顺序升序排序。也可以通过自定义类实现 compareTo() 方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则。并发控制采用的是可重入锁 ReentrantLock

PriorityBlockingQueue 是无界队列,初始化时指定的队列大小。无界体现在后面插入元素的时候,如果空间不够的话会自动扩容

简单地说,它就是 PriorityQueue 的线程安全版本。不可以插入 null 值。同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会阻塞,因为它是无界队列,take 方法在队列为空的时候会阻塞


阻塞队列实现原理

如果队列为空,消费者会阻塞一直等待,当生产者添加元素时,便需要通知消费者当前队列有元素;而队列满时,生产者需要阻塞,消费者消费了队列一个元素后,又需要通知生产者当前队列可用。所以阻塞队列需要让生产者与消费者进行通信,核心是:利用 Condition 实现等待 / 通知机制

下列我们通过 ArrayBlockingQueue 源码来探究如果利用 Condition 实现等待 / 通知机制:

//数据元素数组
final Object[] items;
//内部锁
final ReentrantLock lock;
//消费者监视器
private final Condition notEmpty;
//生产者监视器
private final Condition notFull;  

public ArrayBlockingQueue(int capacity, boolean fair) {
    // ......
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

执行 put 的线程首先需要竞争 lock 锁,没有获取到锁则自旋竞争锁。当往队列里插入一个元素时,如果队列不可用,会调用 Conditionawait 方法阻塞当前线程,直到被消费者唤醒。可以看到 await 方法阻塞生产者主要通过 LockSupport.park(this) 来实现,而 park 方法又是通过调用 unsafe.park 方法来阻塞当前线程,这是一个 native 方法,需要等到 unpark 执行或者线程被中断该方法才会返回。

// ArrayBlockingQueue.put(E e)
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();
    }
}

// ArrayBlockingQueue.enqueue(E e)
private void enqueue(E x) {
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    notEmpty.signal(); // 唤醒一个消费者
}

// ConditionObject.await()
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    long savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this); // 阻塞当前线程
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // ......
}

// LockSupport.park()
public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L); // 阻塞当前线程
    setBlocker(t, null);
}

消费者从队列中获取元素时,若队列为空,会阻塞当前线程;而成功获取元素后,又会唤醒生产者(如果生产者因为队列满而阻塞的话)。

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;
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal(); // 唤醒一个生产者
    return x;
}

参考资料

《Java 并发编程的艺术》

你可能感兴趣的:(并发编程,并发编程,多线程,java)