Java 并发编程之美(一):并发队列 Queue 原理剖析

前言

并发编程是 Java 程序员最重要的技能之一,也是最难掌握的一种技能。而在本人的工作中也经常会接触到并发编程的情况,队列、线程池、线程本地变量等等在本人的工作项目中都有大量的应用,为了更深入的理解并发编程,了解其运行原理,本人决定对工作中接触到的并发编程技术进行进一步的梳理。

本篇是我学习 Java 并发编程系列的开篇,主要内容介绍一下队列 Queue 概念以及方法,然后对工作中常用的几个队列进行分析,介绍其主要使用场景。

什么是队列?

队列是一种特殊的线性表,其插入和删除的操作分别在表的两端进行,队列的特点就是先进先出 (First In First Out)。但这并不是必须的,比如优先度队列就是一个例外,它是以元素的值来排序。我们把向队列中插入元素的过程称为入队 (Enqueue),删除元素的过程称为出队 (Dequeue);并把允许入队的一端称为队尾,允许出队一端称为队头,没有任何元素的队列则称为空队列。通常,队列不允许随机访问队列中的元素。

在 Java5 中新增加了 java.util.Queue 接口,用以支持队列的常见操作。该接口扩展了 java.util.Collection 接口。Queue 接口与 List、Set 同一级别,都是继承了 Collection 接口。

Queue 接口中定义了如下的几个方法:

  • void add(Object e): 将指定元素插入到队列的尾部。如果队列已满,则抛出一个 IllegalStateException 异常。
  • boolean offer(Object e): 将指定的元素插入此队列的尾部。当使用容量有限的队列时,此方法通常比 add(Object e) 有效,如果队列已满,则返回 false。
  • object element(): 获取队列头部的元素,但是不删除该元素。如果队列为空,则抛出一个 NoSuchElementException 异常。
  • Object peek(): 返回队列头部的元素,但是不删除该元素。如果队列为空,则返回 null。
  • Object remove(): 获取队列头部的元素,并删除该元素。如果队列为空,则抛出一个 NoSuchElementException 异常。
  • Object poll(): 返回队列头部的元素,并删除该元素。如果队列为空,则返回 null。

-w468

通用队列 Queue

Queue 接口有一个 PriorityQueue 实现类。除此之外,Queue 还有一个 Deque 接口,Deque 代表一个 “双端队列”,双端队列可以同时从两端删除或添加元素,因此 Deque 可以当作栈来使用。java 为 Deque 提供了 ArrayDeque 实现类和 LinkedList 实现类。

PriorityQueue

PriorityQueue 是一个基于堆结构的优先级队列,它可以根据元素的自然排序或者给定的排序器进行排序。PriorityQueue 是一种比较标准的队列实现类,而不是绝对标准的。这是因为 PriorityQueue 保存队列元素的顺序不是按照元素添加的顺序来保存的,而是在添加元素的时候对元素的大小排序后再保存的。因此在 PriorityQueue 中使用 peek() 或 pool() 取出队列中头部的元素,取出的不是最先添加的元素,而是最小的元素。   

1
2
3
4
5
6
7
PriorityQueue priorityQueue = new PriorityQueue();
priorityQueue.offer(6);
priorityQueue.add(-3);
priorityQueue.add(20);
priorityQueue.offer(18);
// 输出:[-3, 6, 20, 18]
System.out.println(priorityQueue);

PriorityQueue 不允许插入 null 元素,它还需要对队列元素进行排序,PriorityQueue 有两种排序方式:[1]. 自然排序:采用自然排序的 PriorityQueue 集合中的元素必须实现 Compareable 接口,重写 CompareTo 方法,而且应该是一个类的多个实例,否则可能导致 ClassCastException 异常。[2]. 定制排序:创建 PriorityQueue 队列时,传入一个 Comparator 对象,该对象负责对所有队列中的所有元素进行排序。采用定制排序不要求必须实现 Compareable 接口。

总结:[1].Jdk 内置的优先队列 PriorityQueue 内部使用一个堆维护数据,每当有数据 add 进来或者 poll 出去的时候会对堆做从下往上的调整和从上往下的调整。[2].PriorityQueue 不是一个线程安全的类,如果要在多线程环境下使用,可以使用 PriorityBlockingQueue 这个优先阻塞队列。其中 add、poll、remove 方法都使用 ReentrantLock 锁来保持同步,take() 方法中如果元素为空,则会一直保持阻塞。

Deque/ArrayDeque

DeQueue(Double-endedqueue) 为接口,继承了 Queue 接口,创建双向队列,灵活性更强,可以前向或后向迭代,在队头队尾均可心插入或删除元素。它的两个主要实现类是 ArrayDeque 和 LinkedList。

Deque 接口继承自 Queue 接口,但 Deque 支持同时从两端添加或移除元素,因此又被成为双端队列。鉴于此,Deque 接口的实现可以被当作 FIFO 队列使用,也可以当作 LIFO 队列(栈)来使用。官方也是推荐使用 Deque 的实现来替代 Stack。

ArrayDeque 是 Deque 接口的一种具体实现,是依赖于可变数组来实现的。ArrayDeque 没有容量限制,可根据需求自动进行扩容。ArrayDeque 不支持值为 null 的元素。

Deque 接口是 Queue 接口的子接口,它代表一个双端队列,Deque 定义了一些方法:

  • void addFirst(Object e): 将指定元素添加到双端队列的头部。
  • void addLast(Object e): 将指定元素添加到双端队列的尾部。
  • Iteratord descendingItrator(): 返回该双端队列对应的迭代器,该迭代器以逆向顺序来迭代队列中的元素。
  • Object getFirst(): 获取但不删除双端队列的第一个元素。
  • Object getLast(): 获取但不删除双端队列的最后一个元素。
  • boolean offFirst(Object e): 将指定元素添加到双端队列的头部。
  • boolean offLast(OBject e): 将指定元素添加到双端队列的尾部。
  • Object peekFirst(): 获取但不删除双端队列的第一个元素;如果双端队列为空,则返回 null。
  • Object peekLast(): 获取但不删除双端队列的最后一个元素;如果双端队列为空,则返回 null。
  • Object pollFirst(): 获取并删除双端队列的第一个元素;如果双端队列为空,则返回 null。
  • Object pollLast(): 获取并删除双端队列的最后一个元素;如果双端队列为空,则返回 null。
  • Object pop()(栈方法): pop 出该双端队列所表示的栈的栈顶元素。相当于 removeFirst()。
  • void push(Object e)(栈方法): 将一个元素 push 进该双端队列所表示的栈的栈顶。相当于 addFirst()。
  • Object removeFirst(): 获取并删除该双端队列的第一个元素。
  • Object removeFirstOccurence(Object o): 删除该双端队列的第一次出现的元素 o。
  • Object removeLast(): 获取并删除该双端队列的最后一个元素 o。
  • Object removeLastOccurence(Object o): 删除该双端队列的最后一次出现的元素 o。

LinkedList

LinkedList 是 List 接口的实现类,因此它可以是一个集合,可以根据索引来随机访问集合中的元素。此外,它还是 Duque 接口的实现类,因此也可以作为一个双端队列,或者栈来使用。

LinkedList 与 ArrayList、ArrayDeque 的实现机制完全不同,ArrayList 和 ArrayDeque 内部以数组的形式来保存集合中的元素,因此随机访问集合元素时有较好的性能;而 LinkedList 以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但是插入和删除元素时性能比较出色(只需改变指针所指的地址即可),需要指出的是,虽然 Vector 也是以数组的形式来存储集合但因为它实现了线程同步(而且实现的机制不好),故各方面的性能都比较差。

并发队列 Queue

JDK 中常用的并发队列有阻塞队列和非阻塞队列,使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环 CAS 的方式来实现。其中阻塞队列的典型例子是 BlockingQueue,非阻塞队列的典型例子是 ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。

ConcurrentLinkedQueue

ConcurrentLinkedQueue 是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常 ConcurrentLinkedQueue 性能好于 BlockingQueue,它是一个基于链接节点的无界线程安全队列,它采用的是先进先出的规则,当我们增加一个元素时,它会添加到队列的末尾,当我们取一个元素时,它会返回一个队列头部的元素。收集关于队列大小的信息会很慢,需要遍历队列。

BlockingQueue

2010112414472791

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

-w873

  • 抛出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出 IllegalStateException(“Queue full”) 异常。当队列为空时,从队列里获取元素时会抛出 NoSuchElementException 异常。
  • 返回特殊值:插入方法会返回是否成功,成功则返回 true。移除方法,则是从队列里拿出一个元素,如果没有则返回 null。
  • 一直阻塞:当阻塞队列满时,如果生产者线程往队列里 put 元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里 take 元素,队列也会阻塞消费者线程,直到队列可用。
  • 超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。

JDK7 提供了 7 个阻塞队列,分别是:

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

ArrayBlockingQueue

基于数组的阻塞队列实现(有界缓存的等待队列),初始化时必须指定队列的容量。在 ArrayBlockingQueue 内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue 内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。

ArrayBlockingQueue 在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于 LinkedBlockingQueue。在创建 ArrayBlockingQueue 时,我们还可以设置内部的 ReentrantLock 是否使用公平锁,但是公平性会使你在性能上付出代价,只有在的确非常需要的时候再使用它,默认采用非公平锁。

ArrayBlockingQueue 和 LinkedBlockingQueue 间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的 Node 对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于 GC 的影响还是存在一定的区别。

LinkedBlockingQueue

基于链表的阻塞队列实现(可选有界缓存的等待队列),初始化时不需要指定队列的容量,默认是 Integer.MAX_VALUE,也可以看成容量无限大。同 ArrayBlockingQueue 类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue 可以通过构造函数指定该值),才会阻塞生产者线程,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。

而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

作为开发者,我们需要注意的是,如果构造一个 LinkedBlockingQueue 对象,而没有指定其容量大小,LinkedBlockingQueue 会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

PriorityBlockingQueue

基于优先级堆的阻塞队列实现(无界缓存的优先级等待队列),初始化时不需要指定队列的容量,默认是 DEFAULT_INITIAL_CAPACITY=11,最大可分配队列容量 Integer.MAX_VALUE-8。PriorityBlockingQueue 是对 PriorityQueue 的再次包装,队列中的元素按优先级顺序被移除,在实现 PriorityBlockingQueue 时,内部控制线程同步的锁采用的是公平锁。

PriorityBlockingQueue 该类不保证同等优先级的元素顺序,如果你想要强制顺序,就需要考虑自定义顺序或者是 Comparator 使用第二个比较属性。

DelayQueue

基于优先级堆支持的、基于时间的阻塞队列实现(无界缓存的优先级等待队列)。DelayQueue 是一个支持延时获取元素的无界阻塞队列,队列使用 PriorityQueue 来实现,队列中的元素必须实现 Delayed 接口,队列中存放 Delayed 元素,只有在延迟期满后才能从队列中提取元素。当一个元素的 getDelay() 方法返回值小于等于 0 时才能从队列中 poll 元素,否则 poll() 方法会返回 null。

DelayQueue 是一个没有大小限制的队列,应用场景很多,比如对缓存超时的数据进行移除、任务超时处理、空闲连接的关闭等等:

  • 缓存系统的设计:使用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询 DelayQueue,一旦能从 DelayQueue 中获取元素时,就表示有缓存到期了。
  • 定时任务调度:使用 DelayQueue 保存要执行的任务和执行时间,一旦从 DelayQueue 中获取到任务就开始执行,比如 Timer 就是使用 DelayQueue 实现的。

Delayed 接口使对象成为延迟对象,它使存放在 DelayQueue 类中的对象具有了激活日期。该接口强制实现下列两个方法:

  • CompareTo(Delayed o):Delayed 接口继承了 Comparable 接口,因此有了这个方法。
  • getDelay(TimeUnit unit):这个方法返回到激活日期的剩余时间,时间单位由单位参数指定。

DelayQueue 中比较重要的字段如下:

1
2
3
4
5
6
7
8
// 全局独占锁, 用于实现线程安全 [可重入锁]
private final transient ReentrantLock lock = new ReentrantLock();
// 根据 delay 时间排序优先级队列, 用于存储元素, 并按优先级排序
private final PriorityQueue<E> q = new PriorityQueue<E>();
// 用于优化阻塞通知的线程元素 leader
private Thread leader = null;
// 用于实现阻塞和通知的 Condition 对象
private final Condition available = lock.newCondition();

DelayQueue 的大致实现思路:以支持优先级的 PriorityQueue 无界队列作为一个容器,因为元素都必须实现 Delayed 接口,可以根据元素的过期时间来对元素进行排列,因此,先过期的元素会在队首,每次从队列里取出来都是最先要过期的元素。

入队

由于 DelayQueue 不限制长度,添加元素的时候不会因为队列已满产生阻塞,因此 add(E e) 方法以及 put(E e) 方法通过调用 offer(E e) 方法来添加元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public boolean offer(E e) {
// 1. 获取全局独占锁
final ReentrantLock lock = this.lock;
// 2. 执行加锁操作
lock.lock();
try {
// 3. 向优先队列中插入元素
q.offer(e);
// 4. 如果队首元素是刚插入的元素, 则设置 leader 为 null, 并唤醒阻塞在 available 上的线程
if (q.peek() == e) {
leader = null;
available.signal(); // 激活 available 变量条件队列里面的线程
}
return true;
} finally {
// 5. 释放全局独占锁
lock.unlock();
}
}
出队
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public E take() throws InterruptedException {
// 1. 获取全局独占锁
final ReentrantLock lock = this.lock;
// 2. 执行加锁操作
lock.lockInterruptibly();
try {
// 死循环 []
for (;;) {
// 3. 获取优先队列中队首元素
E first = q.peek();
// 4. 队首为空,则阻塞当前线程
if (first == null)
available.await();
else {
// 5. 获取队首元素的超时时间
long delay = first.getDelay(NANOSECONDS);
// 6. 已超时, 调用 poll() 方法弹出该元素, 跳出方法
if (delay <= 0)
return q.poll();
// 7. 释放 first 的引用, 避免内存泄漏
first = null; // don't retain ref while waiting
// 8.leader != null 表明有其他线程在操作, 阻塞当前线程
if (leader != null)
available.await();
else {
// 9.leader 指向当前线程
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 10. 超时阻塞
available.awaitNanos(delay);
} finally {
// 11. 释放 leader
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// 12.leader 为 null 并且队列不为空, 说明没有其他线程在等待, 那就通知条件队列
if (leader == null && q.peek() != null)
available.signal();
// 13. 释放全局独占锁
lock.unlock();
}
}

leader 元素的使用:leader 是等待获取队列头元素的线程,应用主从式设计减少不必要的等待。如果 leader 不等于空,表示已经有线程在等待获取队列的头元素。所以,使用 await() 方法让当前线程等待信号。如果 leader 等于空,则把当前线程设置成 leader,并使用 awaitNanos() 方法让当前线程等待接收信号或等待 delay 时间。

为什么如果不设置 first=null,则会引起内存泄漏呢?线程 A 到达,列首元素没有到期,设置 leader = 线程 A,这是线程 B 来了因为 leader!=null,则会阻塞,线程 C 一样。假如线程阻塞完毕了,获取列首元素成功,出列。这个时候列首元素应该会被回收掉,但是问题是它还被线程 B、线程 C 持有着,所以不会回收,这里只有两个线程,如果有线程 D、线程 E 等呢?这样会无限期的不能回收,就会造成内存泄漏。

SynchronousQueue

SynchronousQueue 是一种无缓冲的等待队列,但是由于该 Queue 本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加;SynchronousQueue 是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素,isEmpty() 方法永远返回是 true,remainingCapacity() 方法永远返回是 0,remove() 和 removeAll() 方法永远返回是 false,iterator() 方法永远返回空,peek() 方法永远返回 null。

声明一个 SynchronousQueue 有两种不同的方式,它们之间有着不太一样的行为。SynchronousQueue 有一个 fair 选项,如果 fair 为 true,称为 fair 模式,否则就是 unfair 模式,公平性使用 TransferQueue,非公平性采用 TransferStack。在 fair 模式下,所有等待的生产者线程或者消费者线程会按照开始等待时间依次排队,然后按照等待先后顺序进行匹配交易。由于 SynchronousQueue 支持公平策略和非公平策略,所以 SynchronousQueue 的底层实现包含两种数据结构:队列(实现公平策略)和栈(实现非公平策略),队列与栈都是通过链表来实现的。

公平策略和非公平策略的区别:如果采用公平策略,SynchronousQueue 会采用公平锁,并配合一个 FIFO(Queue)队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;如果采用非公平策略(SynchronousQueue 默认),SynchronousQueue 采用非公平锁,同时配合一个 LIFO(Stack)队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。一般情况下,FIFO 通常可以支持更大的吞吐量,但 LIFO 可以更大程度的保持线程的本地化。

SynchronousQueue 的一个使用场景是在线程池里。Executors.newCachedThreadPool() 就使用了 SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了 60 秒后会被回收。

LinkedTransferQueue

LinkedTransferQueue 是 JDK1.7 才添加的阻塞队列,基于链表实现的 FIFO 无界阻塞队列,是 ConcurrentLinkedQueue(循环 CAS+volatile 实现的 wait-free 并发算法)、SynchronousQueue(公平模式下转交元素)、LinkedBlockingQueue(阻塞 Queue 的基本方法)的超集。而且 LinkedTransferQueue 更好用,因为它不仅仅综合了这几个类的功能,同时也提供了更高效的实现。

LinkedTransferQueue 采用的一种预占模式。意思就是消费者线程取元素时,如果队列为空,那就生成一个节点(节点元素为 null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为 null 的节点,生产者线程就不入队了,直接就将元素填充到该节点,唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回。

LinkedTransferQueue 类继承自 AbstractQueue 抽象类,并且实现了 TransferQueue 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface TransferQueue<E> extends BlockingQueue<E> {
// 如果存在一个消费者已经等待接收它, 则立即传送指定的元素, 否则返回 false, 并且不进入队列.
boolean tryTransfer(E e);

// 如果存在一个消费者已经等待接收它, 则立即传送指定的元素, 否则等待直到元素被消费者接收.
void transfer(E e) throws InterruptedException;

// 在上述方法的基础上设置超时时间
boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException;

// 如果至少有一位消费者在等待, 则返回 true
boolean hasWaitingConsumer();

// 获取所有等待获取元素的消费线程数量
int getWaitingConsumerCount();
}

我们知道,在普通阻塞队列中,当队列为空时,消费者线程(调用 take 或 poll 方法的线程)一般会阻塞等待生产者线程往队列中存入元素。而 LinkedTransferQueue 的 transfer 方法则比较特殊:1、当有消费者线程阻塞等待时,调用 transfer 方法的生产者线程不会将元素存入队列,而是直接将元素传递给消费者;2、如果调用 transfer 方法的生产者线程发现没有正在等待的消费者线程,则会将元素入队,然后会阻塞等待,直到有一个消费者线程来获取该元素。

和 SynchronousQueue 相比,LinkedTransferQueue 多了一个可以存储的队列,与 LinkedBlockingQueue 相比,LinkedTransferQueue 多了直接传递元素,少了用锁来同步。

LinkedBlockingDeque

LinkedBlockingDeque 是双向链表实现的双向并发阻塞队列。该阻塞队列同时支持 FIFO 和 FILO 两种操作方式,即可以从队列的头和尾同时操作 (插入 / 删除);并且,该阻塞队列是支持线程安全。此外,LinkedBlockingDeque 还是可选容量的 (防止过度膨胀),即可以指定队列的容量。如果不指定,默认容量大小等于 Integer.MAX_VALUE。

-w616

四组不同的行为方式解释:

  • 抛异常:如果试图的操作无法立即执行,抛一个异常。
  • 特定值:如果试图的操作无法立即执行,返回一个特定的值 (常常是 true/false)。
  • 阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
  • 超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功 (典型的是 true/false)。

小知识点

[1]. Queue 的实现通常不允许插入 null 值,除了 LinkedList 这个例外,出于历史原因它允许插入 null 值,但是必须十分注意的是 null 也是 poll 和 peek 方法返回的特别值。
[2]. 虽然 ConcurrentLinkedQueue 的性能很好,但是在调用 size() 方法的时候,会遍历一遍集合,对性能损害较大,执行很慢,因此应该尽量的减少使用这个方法,如果判断是否为空,最好用 isEmpty() 方法。


参考博文

[1]. 并发队列 - 无界阻塞延迟队列 DelayQueue 原理探究


Java 并发编程之美系列


谢谢你长得那么好看,还打赏我!😘
0%