阻塞队列介绍
阻塞队列
前面线程池详解的文中,线程池详解 我们知道线程池的创建需要我们定义队列。不同的线程池,根据使用场景的不同,底层所用的队列也不同。如图不同场景线程所对应的阻塞队列。
- 阻塞队列的作用
阻塞队列,也就是
BlockingQueue它是一个接口,如代码所示:BlockingQueue 继承了public interface BlockingQueue<E> extends Queue<E>{...}Queue接口,是队列的一种。Queue 和 BlockingQueue 都是在Java5 中加入的。 BlockingQueue 是线程安全的,我们在很多场景下都可以利用线程安全的队列来优雅地解决我们业务自身的线程安全问题。比如说,使用生产者/消费者模式的时候,我们生产者只需要往队列里添加元素,而消费者只需要从队列里取出它们就可以了,如图所示:
阻塞队列自身就是线程安全的,所有我们在使用的过程中,不需要自己去考虑更多的线程安全问题。这也就意味着,考虑锁等线程安全问题的重任从“你”转移到了“队列”上,降低了我们开发的难度和工作量。
- 主要并发队列关系图
Queue接口主要的实现类如图,有阻塞队列和非阻塞队列之分,
1.阻塞队列的典型例子就是 BlockingQueue接口的实现类,BlockingQueue 下面有 6 种最主要的实现,分别是 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、DelayQueue、PriorityBlockingQueue 和 LinkedTransferQueue
2.非阻塞并发队列的典型例子是 ConcurrentLinkedQueue,这个类不会让线程阻塞,利用 CAS 保证了线程安全。 还有一个和 Queue 关系紧密的 Deque 接口,它继承了 Queue,如代码所示:
public interface Deque<E> extends Queue<E> {//...}
Deque 的意思是双端队列,是 double-ended-queue 的缩写,它从头和尾都能添加和删除元素;而普通的 Queue 只能从一端进入,另一端出去。这是 Deque 和 Queue 的不同之处,Deque 其他方面的性质都和 Queue 类似。
-
阻塞队列的特点
阻塞队列区别于其他类型的队列的最主要的特点就是“阻塞”这两个字,所以下面重点介绍阻塞功能:阻塞功能使得生产者和消费者两端的能力得以平衡,当有任何一端速度过快时,阻塞队列便会把过快的速度给降下来。实现阻塞最重要的两个方法是 take 方法和 put 方法。
阻塞队列使用
- 阻塞队列常用方法
public interface BlockingQueue<E> extends Queue<E> {
// 增加元素 队列满后操作失败
boolean add(E e);
// 添加元素 队列满返回false
boolean offer(E e);
// 添加元素 队列满 阻塞
void put(E e) throws InterruptedException;
// 添加元素 队列满返回false(含等待时间)
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
// 返回并删除列头元素 如果队列为空,会阻塞
E take() throws InterruptedException;
// 返回并删除列头元素 如果队列为空,
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
// 数据量
int remainingCapacity();
// 返回并删除列头元素 删除识别,抛出异常
boolean remove(Object o);
// 是否包含某元素
public boolean contains(Object o);
// 一次性从BlockingQueue获取所有可用的数据对象
int drainTo(Collection<? super E> c);
// 一次性从BlockingQueue获取所有可用的数据对象,传递最大的获取元素量
int drainTo(Collection<? super E> c, int maxElements);
}
1.常用方法 区别: 不同组的方法,对于特殊情况有不同的处理方式
- 抛出异常: add、remove、element
- 返回结果不抛出异常: offer、poll、peek
- 阻塞:put、take
每一组都有各自的特点。第一组的特点是在无法正常执行的情况下抛出异常;第二组的特点是在无法正常执行的情况下不抛出异常,但会用返回值提示运行失败;第三组的特点是在遇到特殊情况时让线程陷入阻塞状态,等到可以运行再继续执行
- 是否有界(容量有多大) 此外,阻塞队列还有一个非常重要的属性,那就是容量的大小,分为有界和无界两种。 无界队列意味着里面可以容纳非常多的元素,例如 LinkedBlockingQueue 的上限是 Integer.MAX_VALUE,约为2的31次方,是非常大的一个数,可以近似认为是无限容量,因为我们几乎无法把这个容量装满。
但是有的阻塞队列是有界的,例如ArrayBlockingQueue如果容量满了,也不会扩容,所以一旦满了就无法再往里放数据了。
所以我们在使用阻塞队列时,还需要根据不同的场景来选择,使用不当可能造成系统的风险。
常见的阻塞队列
BlockingQueue 接口的实现类都被放在了J.U.C包中,本课时将对常见的和常用的实现类进行介绍,包括 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue,以及 DelayQueue。
- ArrayBlockingQueue
ArrayBlockingQueue 是最典型的有界队列,其内部是用数组存储元素的,利用ReentrantLock 实现线程安全。在创建它的时候就需要指定它的容量,之后也不可以再扩容了,在构造函数中我们同样可以指定是否是公平的,代码如下:
ArrayBlockingQueue(int capacity, boolean fair)
第一个参数是容量,第二个参数是是否公平。正如ReentrantLock一样,如果ArrayBlockingQueue被设置为非公平的,那么就存在插队的可能;如果设置为公平的,那么等待了最长时间的线程会被优先处理,其他线程不允许插队,不过这样的公平策略同时会带来一定的性能损耗,因为非公平的吞吐量通常会高于公平的情况。
-
LinkedBlockingQueue 这是一个内部用链表实现的
BlockingQueue。如果我们不指定它的初始容量,那么它容量默认就为整型的最大值 Integer.MAX_VALUE,由于这个数非常大,我们通常不可能放入这么多的数据,所以 LinkedBlockingQueue 也被称作无界队列,代表它几乎没有界限。 -
SynchronousQueue
SynchronousQueue最大的不同之处在于,它的容量为0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取。 需要注意的是,SynchronousQueue 的容量不是1而是0,因为 SynchronousQueue 不需要去持有元素,它所做的就是直接传递(direct handoff)。由于每当需要传递的时候,SynchronousQueue会把元素直接从生产者传给消费者,在此期间并不需要做存储,所以如果运用得当,它的效率是很高的。
因为SynchronousQueue的容量为0,所以使用时还区别于其他的几个队列。例如 SynchronousQueue 的 peek 方法永远返回 null,代码如下:
public E peek() {
return null;
}
因为 peek 方法的含义是取出头结点,但是 SynchronousQueue 的容量是 0,所以连头结点都没有,peek方法也就没有意义,所以始终返回null。同理,element 始终会抛出NoSuchElementException异常。
而 SynchronousQueue 的 size 方法始终返回 0,因为它内部并没有容量,代码如下:
public int size() {
return 0;
}
直接 return 0,同理,isEmpty 方法始终返回 true:
public boolean isEmpty() {
return true;
}
- PriorityBlockingQueue
前面所说的
ArrayBlockingQueue和LinkedBlockingQueue都是采用先进先出的顺序进行排序,可是如果有的时候我们需要自定义排序怎么办呢?这时就需要使用PriorityBlockingQueue。
PriorityBlockingQueue是一个支持优先级的无界阻塞队列,可以通过自定义类实现compareTo() 方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator来指定排序规则。同时,插入队列的对象必须是可比较大小的,也就是Comparable的,否则会抛出 ClassCastException 异常。
它的take方法在队列为空的时候会阻塞,但是正因为它是无界队列,而且会自动扩容,所以它的队列永远不会满,所以它的 put 方法永远不会阻塞,添加操作始终都会成功,也正因为如此,它的成员变量里只有一个 Condition:
private final Condition notEmpty;
这和之前的 ArrayBlockingQueue 拥有两个 Condition(分别是 notEmpty 和 notFull)形成了鲜明的对比,我们的 PriorityBlockingQueue 不需要 notFull,因为它永远都不会满
- DelayQueue DelayQueue 这个队列比较特殊,具有“延迟”的功能。我们可以设定让队列中的任务延迟多久之后执行,比如 10 秒钟之后执行,这在例如“30分钟后未付款自动取消订单”等需要延迟执行的场景中被大量使用。
它是无界队列,放入的元素必须实现Delayed接口,而Delayed接口又继承了 Comparable 接口,所以自然就拥有了比较和排序的能力,代码如下:
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
可以看出这个Delayed接口继承自 Comparable,里面有一个需要实现的方法,就是 getDelay。这里的getDelay方法返回的是“还剩下多长的延迟时间才会被执行”,如果返回 0 或者负数则代表任务已过期。元素会根据延迟时间的长短被放到队列的不同位置,越靠近队列头代表越早过期。