前言
本篇文章会介绍BlockingQueue的概念。
BlockingQueue
是java线程池的核心。无论是在面试还是工作中,都是必知必会的知识点。
BlockingQueue的名字就完美概括了这个接口的特点,Queue
是BlockingQueue的父接口,说明它本质还是一个队列。Blocking
意为阻塞,说明BlockingQueue相对于普通队列,新增了阻塞的特性。
本文会从BlockingQueue的父接口Queue
开始聊起,包括Queue在java 容器中的定位以及API等等。然后介绍BlockingQueue相比于Queue新增出来的方法。最后会介绍几个BlockingQueue接口的具体实现类的特点和具体用法。
从父接口Queue开始聊起
Queue接口是Java容器大家庭中的一员,具体的定位如下图所示:
Queue自从java 1.5之后,成为接口Collection
下的一员,Queue与List
和Set
最大的区别就在于,Queue是专门为高并发存在的。
除去基本的Collection自带的方法,Queue额外提供了增、删、查的方法。每一种操作又会有两种具体的实现方法,区别在于操作失败时,是抛出错误还是返回某个特定值。
下面对这些方法做一个总结
抛出异常 | 返回特定值 | |
---|---|---|
增 | add() | offer() – 返回false |
删 | remove() | poll() – 返回null |
查 | element() | peek() – 返回null |
注意,其中的查操作和删操作,只能操作队列的头节点,并不能对队列中间的节点执行操作。
BlockingQueue接口及具体实现类
BlockingQueue接口API
由于BlockingQueue是Queue的子接口,所以上面的方法都包含在BlockingQueue中。
而BlockingQueue与Queue的主要区别,在于它新增了阻塞方法 。
什么是阻塞方法呢?简而言之,是当这个操作暂时没法完成的时候,会阻塞住当前线程,直到这个操作完成或者超时。
比如向队列中新增一个元素,如果队列已经满了,这个新增操作就会阻塞住线程,接下来根据选择的具体方法的不同,要么会一直等到队列中有了空位,要么会等超时后返回某个特定值。
BlockingQueue,相对于Queue,新增了两种增加元素和删除元素的阻塞方法,而这些阻塞方法也分成两种类型,一种是一直等待,一种是超时返回特定值。
抛出异常 | 返回特定值 | 阻塞等待 | 超时返回 | |
---|---|---|---|---|
增 | add() | offer() – 返回false | put() | offer(e, time, unit) – 返回false |
删 | remove() | poll() – 返回null | take() | poll(time, unit) – 返回null |
查 | element() | peek() – 返回null | 不存在 | 不存在 |
表中没有标红的方法是继承自Queue接口中的方法,而标红的则是BlockingQueue新增的方法。
具体实现类
BlockingQueue本身也只是个接口,真正发挥作用的是它的具体实现类。
而它的实现类还可以进一步的分类:
如果队列中的元素是按照先进先出排序的,那么可以用LinkedBlockingQueue
和ArrayBlockingQueue
。
如果要自己实现排序原则,就需要PriorityBlockingQueue
和DelayQueue
。
如果要实现添加元素的线程继续追踪该元素直到该元素被取出,就需要SynchronousQueue
和TransferQueue
。
LinkedBlockingQueue 和 ArrayBlockingQueue
这两种具体实现的区别就在于底层的数据结构,一个使用链表而另一个使用数组。
从而也就导致了容量上的不同,理论上LinkedBlockingQueue
的容量理论上是无限的,而ArrayBlockingQueue
的容量受底层的数组限制。
从两者的构造函数就能看出上述区别,ArrayBlockingQueue的构造函数要求必须声明容量上限,而LinkedBlockingQueue可以不声明上限,默认是Integer.MAX_VALUE的值。
PriorityBlockingQueue 和 DelayQueue
Queue本身是先进先出的队列,各个元素在队列里的顺序是按照加入到容器的先后顺序。
但是PriorityBlockingQueue和DelayQueue就可以自定义元素在队列中的排序原则。
PriorityBlockingQueue默认使用元素的compareTo
方法,按从小到大的顺序排列元素。调用take()
会获取到队列中最小的元素。
也可以在调用构造函数时声明自定义的Comparator
,队列中的元素会按照Comparator指定的规则进行排序。
DelayQueue要求队列中的所有的元素都实现Delayed接口,这些元素必须实现的方法是getDelay(TimeUnit unit)
和 compareTo()
方法。
元素在队列中按照compareTo指定的顺序排列。但是,将队列元素取出的操作还依赖于这个元素是否已经过期。这里过期的含义是getDelay()方法返回0或者负数。
比如下面这段代码:
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
long currentTime = System.currentTimeMillis();
DelayedTest a = new DelayedTest(currentTime + 1000, "a");
DelayedTest b = new DelayedTest(currentTime + 200, "b");
DelayedTest c = new DelayedTest(currentTime + 3000, "c");
DelayedTest d = new DelayedTest(currentTime + 400, "d");
DelayQueue<DelayedTest> queue = new DelayQueue<>();
queue.put(a);
queue.put(b);
queue.put(c);
queue.put(d);
for (int i = 0; i < 4; i++) {
System.out.println(queue.take());
}
}
}
class DelayedTest implements Delayed {
final private long wait;
final private String name;
public DelayedTest(long wait, String name) {
this.wait = wait;
this.name = name;
}
@Override
public int compareTo(Delayed o) {
if (o.getDelay(TimeUnit.MILLISECONDS) < this.getDelay(TimeUnit.MILLISECONDS)) {
return 1;
} else if (o.getDelay(TimeUnit.MILLISECONDS) > this.getDelay(TimeUnit.MILLISECONDS)) {
return -1;
} else {
return 0;
}
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.wait - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public String toString() {
return "DelayedTest{" +
"name='" + name + '\'' +
'}';
}
}
代码分析:
这段代码中用DelayedTest
类实现了Delayed接口
,也就是实现了两个方法getDelay()
和compareTo()
。
其中,getDelay方法是定义了当前的对象还有多长时间进入“过期”状态,compareTo方法定义了,越早过期的对象排在前面,晚过期的对象排在后面。
在main方法中创建了4个对象,以当前系统时间为起点,分别在1000ms, 200ms, 3000ms, 400ms后进入过期状态。
输出的时候可以看到,输出语句之间有时间间隔,最后一句是在运行3秒后执行的。由此可见,只有在元素进入过期状态,take()
方法才会把元素从队列中取出。
下图是执行结果:
可以说,DelayedQueue是一种特殊的PriorityBlockingQueue,同时又增加了新的功能,就是元素未过期不能被取出队列。
SynchronousQueue 和 TransferQueue
这两个实现类是很有趣的。因为这两个类中,可以实现用取出方法来阻塞添加方法。
什么意思呢?相比于别的具体实现类,只要队列不满,一个线程如果成功把对象添加到队列后,就会去执行其他操作,不再管这个元素的后续结果。
在SynchronousQueue和TransferQueue中,就实现了添加元素的线程对该元素的追踪效果。当一个线程向这两种队列添加一个元素时,这个线程进入阻塞状态,直到有其他线程将这个元素从队列中取走,这个线程才会继续执行。
而这两种具体实现的区别在于,
SynchronousQueue
的队列容量为零,可以理解为这个队列并不是真实存在的。也就是说,任何线程调用put()
方法向这个队列中添加数据,都会被阻塞住,直到其他线程调用take()方法才会唤醒这个线程。 对take()方法也是如此,如果一个线程先调用了take()方法,也会被阻塞住,直到另外一个线程向队列中put()了一个元素。
TransferQueue
的容量并不为零,并且保留了非阻塞式的put()方法,允许其他线程将元素加入到队列中就去执行别的操作。也添加了阻塞式的transfer(element)
方法,当一个线程调用transfer方法后,进入阻塞状态,直到这个元素被其他线程take走。
看下面这段对TransferQueue的测试代码:
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
TransferQueue<Integer> queue = new LinkedTransferQueue();
(new Thread(new Runnable() {
@Override
public void run() {
try {
while(true) {
TimeUnit.SECONDS.sleep(1);
System.out.println("取出了" + queue.take());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "takeThread")).start();
queue.put(1);
System.out.println("调用put方法");
queue.transfer(2);
System.out.println("调用transfer方法");
}
}
上面代码的输出如下:
代码分析:
代码中创建了一个元素类型是Integer的TransferQueue
,分别调用put()
方法和transfer()
方法把1和2加入到队列中。
调用完put方法和transfer方法后,会执行一句打印语句。
此外还有一个线程takeThread
,每隔一秒就会调用take()
方法从队列中取出元素。
根据上述代码的输出可以得出下面结论:
在调用完put()方法后直接执行了put后的打印语句,然后执行了元素的取出操作,说明put方法没有阻塞。
而transfer()
方法的打印语句发生在取出元素之后,说明是取出元素之后,打印语句才得到执行。也就是说,线程执行完transfer()方法后,就进入了阻塞状态,直到添加的元素被取出后才重新被唤醒。
总结
本文从Queue出发,讲了BlockingQueue相对于Queue的提升。
然后介绍了六种BlockingQueue具体实现类的用法和特点。
BlockingQueue的主要应用场景是在线程池中,可以根据具体的业务场景,选择不同的BlockingQueue的具体实现类用在线程池中。