【JUC 并发编程】— 阻塞队列

Queue

阻塞队列(BlockingQueue)继承自 Queue,先看看 Queue 定义了哪些接口

/**
 * Queue 相对于 Collection 中基本的方法,提供了另外的插入,移除和检查方法。
 * 每个方法都有两种形式:如果操作失败,一种是直接抛异常,另一种是返回特殊值(null 或者 false)
 */
public interface Queue<E> extends Collection<E> {
    /**
     * 添加元素,如果队列已满,则抛出 IllegalStateException 异常
     */
    boolean add(E e);
    
    /**
     * 添加元素
     * 添加成功,返回 true,否则 false
     * 如果是有界队列,此方法优于 add() 方法,因为 add() 失败了只能抛异常
     */
    boolean offer(E e);
    
    /**
     * 移除第一个元素
     * 如果队列为空,则抛出 NoSuchElementException 异常
     */
    E remove();
    
    /**
     * 移除第一个元素
     * 如果队列为空,返回 null
     */
    E poll();
    
    /**
     * 返回第一个元素,但不会移除
     * 如果队列为空,则抛出 NoSuchElementException 异常
     */
    E element();
    
    /**
     * 返回第一个元素,但不会移除
     * 如果队列为空,则返回 null
     */
    E peek();
}

BlockingQueue

JDK 中是这么介绍 BlockingQueue 的

A {@link java.util.Queue} that additionally supports operations
that wait for the queue to become non-empty when retrieving an
element, and wait for space to become available in the queue when
storing an element.

在 Queue 的基础上支持另外两种操作:① 支持阻塞的插入方法:当队列满时,插入线程会被阻塞,直到队列不满。② 支持阻塞的移除方法:队列为空时,获取元素的线程会等待,直到队列为非空。

也就是对应着 put() 和 take() 方法

/**
 * 插入元素
 * 如果队列满了,则阻塞插入线程
 */
void put(E e) throws InterruptedException;

/**
 * 移除元素
 * 如果队列为空,则阻塞移除线程,直到队列非空
 */
E take() throws InterruptedException;

Doug Lea 老爷子也是贴心,在 BlockingQueue 源码注释中为我们整理了这些方法

Throws exception Special value Blocks Times out
Insert add(e) offer(e) put(e) offer(e, time, unit)
Remove remove() poll() take() poll(time, unit)
Examine element() peek() not applicable not applicable

阻塞队列实现

JDK 7 提供了 7 个阻塞队列实现,如下

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

ArrayBlockingQueue

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();
    notFull =  lock.newCondition();
}

而且可以看到,公平性是使用重入锁来实现的。

LinkedBlockingQueue

LinkedBlockingQueue 是一个用链表实现的有界阻塞队列,默认长度为 Integer.MAX_VALUE

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

PriorityBlockingQueue

PriorityBlockingQueue 是一个支持优先级的无界阻塞队列,默认情况元素采用自然顺序升序排列。

/**
 * The comparator, or null if priority queue uses elements'
 * natural ordering.
 */
private transient Comparator<? super E> comparator;

public PriorityBlockingQueue(int initialCapacity,
                             Comparator<? super E> comparator) {
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.lock = new ReentrantLock();
    this.notEmpty = lock.newCondition();
    this.comparator = comparator;
    this.queue = new Object[initialCapacity];
}

可以自定义类实现 compareTo() 方法来指定元素排序规则,或者初始化 PriorityBlockingQueue 时,指定构造函数 Comparator 来对元素进行排序。

DelayQueue

DelayQueue 是一个支持延时获取元素的无界阻塞队列。队列使用 PriorityQueue 来实现,队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能队列中获取当前元素。只有在延迟期满时才能从队列中获取元素。

SynchronousQueue

SynchronousQueue 是一个不存储元素的阻塞队列。每一个 put 操作必须对应一个 take 操作,否则不能继续添加元素。SynchronousQueue 维护一组线程,这些线程在等待着把元素加入或移除队列。这种实现队列的方式看似很奇怪,但由于可以直接交付工作,从而降低了将数据从生产者移动到消费者的延迟。(在传统的队列中,在一个工作单元可以交付之前,必须通过串行方式首先完成入列[Enqueue]或者出列[Dequeue]等操作)。直接交付方式还会将更多关于任务状态的信息反馈给生产者。当交付被接受时,他就知道消费者已经得到了任务,而不是简单地把任务放入一个队列。仅当有足够多的消费者,并且总是有一个消费者准备好获取交付的工作时,才适合使用同步队列。SynchronousQueue 的吞吐量高于 LinkedBlockingQueue 和 ArrayBlockingQueue。

LinkedTransferQueue

LinkedTransferQueue 是一个由链表结构组成的无界阻塞队列。相对于其他阻塞队列,LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。

transfer 方法

如果当前有消费者正在等待接收元素(消费者使用 take() 方法或者带时间限制的 poll() 方法时),transfer 方法可以把生产者传入的元素立刻 transfer 给消费者。如果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 节点,并等到该元素被消费者消费了才返回。

tryTransfer 方法

tryTransfer 方法是用来试探生产者传入的元素是否能够直接传给消费者。如果没有消费者等待接收元素,则返回 false。和 transfer 方法的区别是 tryTransfer 方法无论消费者是否接收,方法立即返回,而 transfer 方法是必须等到消费者消费了才返回。

LinkedBlockingDeque

LinkedBlockingDeque 是一个由链表结构组成的双向阻塞队列。所谓双向队列指的是可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。

阻塞队列原理

JDK 使用通知模式来实现生产者线程和消费者线程之间的通信。以 ArrayBlockingQueue 为例

/** 队列元素数组 */
final Object[] items;
/** 队列元素数量 */
int count;

/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;

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

等待通知采用的是 Condition 的 await() 方法和 signal() 方法(Condition 源码解析看这里)。

重点看看 put() 方法和 take() 方法

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        insert(e);
    } finally {
        lock.unlock();
    }
}

private void insert(E x) {
    items[putIndex] = x;
    putIndex = inc(putIndex);
    ++count;
    notEmpty.signal();
}

put() 方法思路:获取锁,如果队列已满,则阻塞生产者线程;如果队列没满,则调用 insert() 方法插入,这时候队列肯定非空,那么就执行 notEmpty.signal() 通知消费者线程。

再来看看 take() 方法

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return extract();
    } finally {
        lock.unlock();
    }
}

private E extract() {
    final Object[] items = this.items;
    E x = this.<E>cast(items[takeIndex]);
    items[takeIndex] = null;
    takeIndex = inc(takeIndex);
    --count;
    notFull.signal();
    return x;
}

take() 方法思路:获取锁,如果队列为空,则阻塞消费者线程,否则移除元素。这时候队列没满,则执行 notFull.signal() 通知生产者线程。

参考

posted @ 2022-06-08 18:36  Tailife  阅读(29)  评论(0编辑  收藏  举报