Java并发编程-阻塞队列BlockingQueue

854 阅读4分钟

这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战

前言

什么是阻塞队列?有那7种阻塞队列?为什么需要阻塞队列?通过剖析JDK 1.8 源码了解源码设计的精髓和巧妙的地方,并运用在我们实际的项目中,他山之石,可以攻玉。

1. 什么是阻塞队列

image.png 感觉并发编程上面的解释不是很容易理解,我们看下源码Doug Lea大佬的注释,意思是阻塞队列支持检索的时候可以等待队列变成非空(也就是阻塞等待), 存储元素的时候,等待空间可用。
BlockingQueue方法有四种形式,具有不同的处理操作的方法,这些操作不能立即得到满足,但可能在将来的某个时候得到满足:

  1. 抛出一个异常
  2. 返回一个特殊值(null 或 false,依赖具体的操作)
  3. 在操作可以成功前,无限期地阻塞当前线程
  4. 在放弃前只在给定的最大时间限制内阻塞。

image.png

2. JDK种7种阻塞队列

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

  1. ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
  2. LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
  3. PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
  4. DelayQueue:一个使用优先级队列实现的无界阻塞队列。
  5. SynchronousQueue:一个不存储元素的阻塞队列。
  6. LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
  7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

2.1 ArrayBlockingQueue

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

image.png 看下ArrayBlockingQueue 依赖继承关系,底层是以数组Object[] items;保存元素的。

2.2 LinkedBlockingQueue

LinkedBlockingQueue是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为 Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。

2.3 PriorityBlockingQueue

PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序 升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。

2.4 DelayQueue

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

使用场景:

  1. 缓存系统的设计
  2. 定时任务调度

2.5 SynchronousQueue

SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。

它支持公平访问队列。默认情况下线程采用非公平性策略访问队列。使用以下构造方法可以创建公平性访问的SynchronousQueue,如果设置为true,则等待的线程会采用先进先出的顺序访问队列。

public SynchronousQueue() {
    this(false);
}

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

2.6 LinkedTransferQueue

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

2.6.1 transfer方法

2.6.2 tryTransfer方法

2.7 LinkedBlockingDeque

LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。

3. 阻塞队列的实现原理

我们初学线程池可能会有一个疑问,如果队列是空的,消费者会一直等待,当生产者添加元素时,消费者是如何知道当前队列有元素的呢? 如果是你你会怎么去设计?

使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。

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

我们看下put操作,源码不复杂,我简单解释一下while条件,判断如果队列满了, notFull.await() 等待,当队列不满了,继续执行入队操作。

我们可能比较疑惑为什么用while不是用if判断呢,因为多线程的情况下,其他线程先入队 条件队列的await和single,可以参考我之间的 Java并发编程-AQS源码之条件队列

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

take方法

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        // 如果队列无元素,则阻塞等待在条件notEmpty上
        while (count == 0)
            notEmpty.await();
            
        // 出队列    
        return dequeue();
    } finally {
        lock.unlock();
    }
}

参考

《Java并发编程艺术》