阻塞队列
阻塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
非阻塞队列中的几个主要方法:
-
add(E e)
: 将元素 e 插入到队列末尾,如果插入成功,则返回 true;如果插入失败(即队列已满),则会抛出异常 -
remove()
: 移除队首元素,若移除成功,则返回 true;如果移除失败(队列为空),则会抛出异常 -
offer(E e)
:将元素 e 插入到队列末尾,如果插入成功,则返回 true;如果插入失败(即队列已满),则返回 false -
poll()
:移除并获取队首元素,若成功,则返回队首元素;否则返回 null -
peek()
:获取队首元素,若成功,则返回队首元素;否则返回 null
对于非阻塞队列,一般情况下建议使用 offer、poll 和 peek 三个方法,不建议使用 add 和 remove 方法。因为使用 offer、poll 和 peek 三个方法可以通过返回值判断操作成功与否,而使用 add 和 remove 方法却不能达到这样的效果。注意,非阻塞队列中的方法都没有进行同步措施。
阻塞队列中的几个主要方法:
阻塞队列包括了非阻塞队列中的大部分方法,上面列举的 5 个方法在阻塞队列中都存在,但是要注意这 5 个方法在阻塞队列中都进行了同步措施。除此之外,阻塞队列提供了另外 4 个非常有用的方法:
-
put(E e)
方法用来向队尾存入元素,如果队列满,则等待 -
take()
方法用来从队首取元素,如果队列为空,则等待 -
offer(E e,long timeout, TimeUnit unit)
方法用来向队尾存入元素,如果队列满,则等待一定的时间,当时间期限达到时,如果还没有插入成功,则返回 false;否则返回 true -
poll(long timeout, TimeUnit unit)
方法用来从队首取元素,如果队列空,则等待一定的时间,当时间期限达到时,如果没取到,则返回 null;否则返回取得的元素
Java 里的阻塞队列
- ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
除了这几个外还有一个
ScheduledThreadPoolExecutor.DelayedWorkQueue
这个其实是 DelayQueue 的优化
ArrayBlockingQueue
ArrayBlockingQueue 是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列:
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
访问者的公平性是使用可重入锁实现的
LinkedBlockingQueue
LinkedBlockingQueue 是一个用链表实现的有界阻塞队列。此队列按照先进先出的原则对元素进行排序。
容易被误解为无边界,但其实其行为和内部代码都是基于有界的逻辑实现的,只不过如果我们没有在创建队列时就指定容量,那么其容量限制就自动被 设置为 Integer.MAX_VALUE ,成为了无界队列。
PriorityBlockingQueue
PriorityBlockingQueue 是一个支持优先级的无界队列。底层采用数组实现了二叉堆,相比 PriorityQueue 其实就是将默认情况下元素采取自然顺序排列,也可以通过比较器 comparator 来指定元素的排序规则。
DelayQueue
这个队列还是挺有意思的,DelayQueue 是一个支持延时获取元素的无界阻塞队列。内部使用 PriorityQueue 来存储元素。队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中取出元素。我们可以将 DelayQueue 运用在以下应用场景:
- 缓存系统的设计:可以用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询 DelayQueue,一旦能从 DelayQueue 中获取(删除)元素时,表示缓存有效期到了。
- 定时任务调度。使用 DelayQueue 保存当天将会执行的任务和执行时间,一旦从 DelayQueue 中获取到任务就开始执行,从比如 TimerQueue 就是使用 DelayQueue 实现的。
队列中的 Delayed 必须实现 compareTo 来指定元素的顺序。比如让延时时间最长的放在队列的末尾。实现代码如下:
如何实现 Delayed 接口
我们可以参考 ScheduledThreadPoolExecutor 里 ScheduledFutureTask 类。这个类实现了 Delayed 接口。
然后使用 getDelay 可以查询当前元素还需要延时多久,代码如下:
public long getDelay(TimeUnit unit) {
return unit.convert(time - now(), TimeUnit.NANOSECONDS);
}
通过构造函数可以看出延迟时间参数 ns 的单位是纳秒,自己设计的时候最好使用纳秒,因为 getDelay 时可以指定任意单位,一旦以纳秒作为单位,而延时的时间又精确不到纳秒就麻烦了。使用时请注意当 time 小于当前时间时,getDelay 会返回负数。
如何实现延时队列
延时队列的实现很简单,当消费者从队列里获取元素时,如果元素没有达到延时时间,就阻塞当前线程。
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class DelayedEle implements Delayed {
private final long delayTime; //延迟时间
private final long expire; //到期时间
private String data; //数据
public DelayedEle(long delay, String data) {
delayTime = delay;
this.data = data;
expire = System.currentTimeMillis() + delay;
}
/**
* 剩余时间=到期时间-当前时间
*/
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.expire - System.currentTimeMillis() , TimeUnit.MILLISECONDS);
}
/**
* 优先队列里面优先级规则
*/
@Override
public int compareTo(Delayed o) {
//根据 delay 时间
return (int) (this.getDelay(TimeUnit.MILLISECONDS) -o.getDelay(TimeUnit.MILLISECONDS));
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("DelayedElement{");
sb.append("delay=").append(delayTime);
sb.append(", expire=").append(expire);
sb.append(", data='").append(data).append('\'');
sb.append('}');
return sb.toString();
}
}
测试
import java.util.concurrent.DelayQueue;
/**
* @author imlgw.top
* @date 2019/8/7 17:55
*/
public class DelayedQueueTest {
public static void main(String[] args) throws InterruptedException {
DelayQueue<DelayedEle> delayQueue = new DelayQueue<DelayedEle>();
DelayedEle element1 = new DelayedEle(3000,"lgw");
DelayedEle element2 = new DelayedEle(3000,"top");
delayQueue.offer(element1);
delayQueue.offer(element2);
long l = System.currentTimeMillis();
System.out.println(l);
element1 = delayQueue.take();
System.out.println(System.currentTimeMillis()-l);
System.out.println(element1);
}
}
/** 测试结果
* 1565176036796
* 3016
* DelayedElement{delay=3000, expire=1565176039796, data='lgw'}
*/
SynchronousQueue
SynchronousQueue 是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。SynchronousQueue 可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用,SynchronousQueue 的吞吐量高于 LinkedBlockingQueue 和 ArrayBlockingQueue。
线程池工厂方法 newCachedThreadPool 底层就是用的 Synchronized
LinkedTransferQueue
TransferQueue 相比 SynchronousQueue 用处更广、更好用,因为你可以决定是使用 BlockingQueue 的方法(译者注:例如 put 方法)还是确保一次传递完成(译者注:即 transfer 方法)。在队列中已有元素的情况下,调用 transfer 方法,可以确保队列中被传递元素之前的所有元素都能被处理。Doug Lea 说 从功能角度来讲,LinkedTransferQueue 实际上是 ConcurrentLinkedQueue、SynchronousQueue(公平模式)和 LinkedBlockingQueue 的超集。而且 LinkedTransferQueue 更好用,因为它不仅仅综合了这几个类的功能,同时也提供了更高效的实现。
LinkedTransferQueue 是一个由链表结构组成的无界阻塞 TransferQueue 队列。相对于其他阻塞队列 LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。
LinkedTransferQueue 与 SynchronousQueue 中公平模式的实现 TransferQueue 是一样的,队列中存放的不是数据,而是操作(取出数据的操作 take 和放入数据的操作 put)队列中既可以存放 take 操作也可以存放 put 操作,但是队列中不能同时存在两种不同的操作,因为不同的操作会触发队列进行配对(操作出队)。
transfer 方法。如果当前有消费者正在等待接收元素(消费者使用 take() 方法或带时间限制的 poll() 方法时),transfer 方法可以把生产者传入的元素立刻 transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 节点,并等到该元素被消费者消费了才返回。transfer 方法的关键代码如下:
private E xfer(E e, boolean haveData, int how, long nanos) {
if (haveData && (e == null))
throw new NullPointerException();
Node s = null; // the node to append, if needed
retry:
for (;;) { // restart on append race
for (Node h = head, p = h; p != null;) { // find & match first node
boolean isData = p.isData;
Object item = p.item;
if (item != p && (item != null) == isData) { // unmatched
if (isData == haveData) // can't match
break;
if (p.casItem(item, e)) { // match
for (Node q = p; q != h;) {
Node n = q.next; // update by 2 unless singleton
if (head == h && casHead(h, n == null ? q : n)) {
h.forgetNext();
break;
} // advance and retry
if ((h = head) == null ||
(q = h.next) == null || !q.isMatched())
break; // unless slack < 2
}
LockSupport.unpark(p.waiter);
return LinkedTransferQueue.<E>cast(item);
}
}
Node n = p.next;
p = (p != n) ? n : (h = head); // Use head if p offlist
}
if (how != NOW) { // No matches available
if (s == null)
s = new Node(e, haveData);
//(1)尝试添加到链表尾部
Node pred = tryAppend(s, haveData);
if (pred == null)
continue retry; // lost race vs opposite mode
if (how != ASYNC)
//(2)等待消费者消费
return awaitMatch(s, pred, e, (how == TIMED), nanos);
}
return e; // not waiting
}
}
(1)代码是试图把存放当前元素的 s 节点作为 tail 节点。(2)代码是让 CPU 自旋等待消费者消费元素。因为自旋会消耗 CPU,所以自旋一定的次数后使用 Thread.yield() 方法来暂停当前正在执行的线程,并执行其他线程。
tryTransfer 方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回 false。和 transfer 方法的区别是 tryTransfer 方法无论消费者是否接收,方法立即返回。而 transfer 方法是必须等到消费者消费了才返回。
对于带有时间限制的 tryTransfer(E e, long timeout, TimeUnit unit) 方法,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回 false,如果在超时时间内消费了元素,则返回 true。
实例
package juc_study.collection.blocking;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedTransferQueue;
public class LinkedTransferQueueDemo {
static LinkedTransferQueue<String> lnkTransQueue = new LinkedTransferQueue<String>();
public static void main(String[] args) {
ExecutorService exService = Executors.newFixedThreadPool(2);
Producer producer = new LinkedTransferQueueDemo().new Producer();
Consumer consumer = new LinkedTransferQueueDemo().new Consumer();
exService.execute(producer);
exService.execute(consumer);
exService.shutdown();
}
class Producer implements Runnable{
@Override
public void run() {
for(int i=0;i<3;i++){
try {
System.out.println("Producer is waiting to transfer...");
lnkTransQueue.transfer("A"+i);
System.out.println("producer transfered element: A"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable{
@Override
public void run() {
for(int i=0;i<3;i++){
try {
System.out.println("Consumer is waiting to take element...");
String s= lnkTransQueue.take();
System.out.println("Consumer received Element: "+s);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
LinkedBlockingDeque
LinkedBlockingDeque 是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque 多了 addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast 等方法,以 First 单词结尾的方法,表示插入,获取(peek)或移除双端队列的第一个元素。以 Last 单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。另外插入方法 add 等同于 addLast,移除方法 remove 等效于 removeFirst。但是 take 方法却等同于 takeFirst,不知道是不是 Jdk 的 bug,使用时还是用带有 First 和 Last 后缀的方法更清楚。在初始化 LinkedBlockingDeque 时可以初始化队列的容量,用来防止其再扩容时过渡膨胀。另外双向阻塞队列可以运用在“工作窃取”模式中。