Java并发包下的阻塞队列

本文简要介绍一下什么是阻塞队列,Java并发包给我们提供的阻塞队列有哪些,以及怎么去简单使用

阻塞队列 BlockingQueue

1. 简单概念

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

Java并发包下的阻塞队列_第1张图片

  • 支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列变为不满

  • 支持阻塞的移除方法:当队列为空时,获取元素的线程会等待队列变为非空

阻塞队列的使用场景: 常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、生产者用来获取元素的容器

2. API介绍

BlockingQueue提供子类实现的几个API如下:

  • add()和remove()
  • offer()和poll()
  • put()和take()
// 将指定元素插入队列,成功返回true;不成功返回false
boolean add(E e);

// 从队列中移出指定元素并返回该元素
boolean remove(Object o);

// 将指定元素插入队列,在使用有容量限制的队列时,该方法优于add()
boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

// 获取队列的头元素并将其从队列中移除,如果队列为空返回null     
E poll(long timeout, TimeUnit unit)
        throws InterruptedException;

// 将指定的元素插入队列中
void put(E e) throws InterruptedException;

// 获取队列的头元素并将其从队列中移除
E take() throws InterruptedException;

阻塞队列的方法总结如下:

方法/处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e, time, unit)
移除方法 remove(e) poll() take() poll(time, unit)
  • 抛出异常:当队列满时往队列里面插入元素,会抛出 IllegalStateException 异常;当队列为空时从队列里面获取元素会抛出 NoSuchElementException 异常

  • 返回特殊值:当往队列里插入元素时会返回元素是否插入成功,成功则返回true;移除方法,会获取队列的头元素并将其从队列中移除,如果队列为空返回null

  • 一直阻塞:当队列满时,如果生产者线程往队列里面put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出;当队列为空时,如果消费者线程从队列里take元素,则队列会阻塞住消费者线程,直到队列不为空。

  • 超时退出:当阻塞队列满时,如果生产者线程往队列里面插入元素,队列会阻塞生产者线程一段时间,超过指定时间后生产者线程将会退出

注意: 上面所说的队列满时插入元素的线程阻塞是针对有界队列的,如果是无界队列,队列不会出现满的情况,所以使用put或offer方法永远不会被阻塞,而且使用offer()方法会永远返回true。

BlockingQueue的实现类——七大阻塞队列

下图是BlockingQueue的继承体系:

Java并发包下的阻塞队列_第2张图片

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

 ArrayBlockingQueue是一个由数组实现的有界阻塞队列。按照FIFO的原则对元素进行排序。队列使用可重入锁(ReentrantLock)对同步资源加锁,默认情况下不保证线程公平的访问队列(非公平锁)。下面是ArrayBlockingQueue源码中的几个成员变量:

// 保存队列元素的数组
final Object[] items;

// 将要取出元素的索引
int takeIndex;

// 将要添加元素的索引
int putIndex;

// 队列中已添加元素的数量
int count;

// 可重入锁
final ReentrantLock lock;

// 取元素线程等待队列:队列非空可获取
private final Condition notEmpty;

// 插入元素线程等待队列:队列非满可插入
private final Condition notFull;

如下是ArrayBlockingQueue的构造器源码:

// 传入一个容量
public ArrayBlockingQueue(int capacity) {
    // 默认是非公平的
    this(capacity, false);
}

// 可以传入一个boolean值,true是公平锁
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的put()和take()方法的源码:

  • put() 方法
// 插入元素
public void put(E e) throws InterruptedException {
    // 检查掺入的元素是否为空,如果为空抛出NullPointerException异常
    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;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    // 唤醒取元素线程,可以取元素了
    notEmpty.signal();
}
  • 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;
}

使用ArrayBlockingQueue实现一个简单的生产消费场景:

public class TestArrayBlockingQueue {

    public static void main(String[] args) {
        // 容量为3的阻塞队列
        final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);

        // 两个生产者线程生产数据
        for (int i = 0; i < 2; i++) {
            new Thread() {
                @Override
                public void run() {
                    while (true) {
                        try {
                            TimeUnit.SECONDS.sleep(1);
                            System.out.println(Thread.currentThread().getName() + " 准备插入数据 " +
                                    (queue.size() == 3 ? "...队列已满,正在等待..." : "..."));
                            queue.put(1);
                            System.out.println(Thread.currentThread().getName() + " 插入数据," +
                                    "队列目前有 " + queue.size() + " 个数据.");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }.start();
        }
        
        // 一个消费者线程消费数据
        new Thread() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(Thread.currentThread().getName() + " 准备取出数据 " +
                            (queue.size() == 0 ? "...队列已空,正在等待..." : "..."));
                    queue.take();
                    System.out.println(Thread.currentThread().getName() + " 取出数据," +
                            "队列目前有 " + queue.size() + " 个数据.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
}

结果如下:
Thread-2 准备取出数据 ...队列已空,正在等待...
Thread-0 准备插入数据 ...
Thread-1 准备插入数据 ...
Thread-1 插入数据,队列目前有 2 个数据.
Thread-0 插入数据,队列目前有 2 个数据.
Thread-2 取出数据,队列目前有 1 个数据.
Thread-0 准备插入数据 ...
Thread-1 准备插入数据 ...
Thread-0 插入数据,队列目前有 2 个数据.
Thread-1 插入数据,队列目前有 3 个数据.
Thread-0 准备插入数据 ...队列已满,正在等待...
Thread-1 准备插入数据 ...队列已满,正在等待...
2. LinkedBlockingQueue

 LinkedBlockingQueue 是一个由链表实现的有界阻塞队列。队列按照FIFO的原则对元素进行排序,队列默认的最大长度是:Integer.MAX_VALUE。该队列也是使用 ReentrantLock 对同步资源加锁。在线程池中,LinkedBlockingQueue是FixedThreadPool和SingleThreadExecutor的工作队列。下面是它的构造方法源码:

// 默认构造器
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);
}
3. LinkedBlockingDeque

 LinkedBlockingDeque 是一个由链表结构组成的双向有界阻塞队列:可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,减少了一半的竞争。该队列也是使用 ReentrantLock 对同步资源加锁。LinkedBlockingDeque 与其他阻塞队列相比,多了 addFist、addLast、offerFist、offerLast、peekFirst、peekLast等方法:以First结尾的方法表示插入、获取或移除双向队列的第一个元素;以Last结尾的方法表示插入、获取或移除双向队列的最后一个元素。下面是其构造器源码:

// 默认构造器
public LinkedBlockingDeque() {
    this(Integer.MAX_VALUE);
}

// 可设置容量的构造器
public LinkedBlockingDeque(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
}
4. PriorityBlockingQueue

 PriorityBlockingQueue 是一个支持优先级的无界阻塞队列:默认情况下元素采取升序排列,也可以通过自定义类实现compareTo()方法来制定元素的排序规则,或者在初始化时制定构造参数Comparator来对元素进行排序。该队列也是使用 ReentrantLock 对同步资源加锁。下面是其构造器源码:

// 默认构造器
public PriorityBlockingQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}

// 定制化排序
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];
}
5. SynchronousQueue

 SynchronousQueue 是一个不存储元素的阻塞队列:每一个put操作必须等待一个take操作,否则不能继续添加元素。该队列也是使用 ReentrantLock 对同步资源加锁,默认情况下线程采用非公平策略访问队列。SynchronousQueue 的身份类似于一个中转者,负责把生产者线程处理的数据直接传递给消费者线程,队列本身并不存储任何元素,非常适合传递性场景。

6. LinkedTransferQueue

 LinkedTransferQueue 是一个由链表组成的无界阻塞队列。该队列也是使用 ReentrantLock 对同步资源加锁。相比于其他队列,该队列多了tryTransfer和transfer方法:

  • transfer方法:如果当前消费者正在等待接收元素,transfer方法可以把生产者传入的元素立刻传输给消费者。如果没有消费者等待接收元素,transfer方法会将元素存放在队列的tail节点(尾节点),并等到该元素被消费者消费了才返回。

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

7. DelayQueue

 DelayQueue 是一个支持支持延时获取元素的无界阻塞队列:队列使用PriorityQueue来实现。该队列也是使用 ReentrantLock 对同步资源加锁。队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素,只有在延时期满时才能从队列中提取元素。

DelayQueue的使用场景:

  • 缓存系统的设计:使用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询 DelayQueue,一旦能从 DelayQueue 中获取元素时,表示缓存有效期已到。

  • 定时任务调度:使用 DelayQueue 保存当天将会执行的任务和执行时间,一旦从 DelayQueue 中获取到任务就开始执行。

参考

《Java并发编程的艺术》

你可能感兴趣的:(◆【编程语言】)