Java核心技术18 | 并发包中的常用队列
有时候我们把并发包下面的所有容器都习惯叫作并发容器,但是严格来讲,只有以“Concurrent”为前缀的容器才是真正的并发容器。
- Concurrent类型基于lock-free,在常见的多线程访问场景,一般可以提供较高吞吐量。
- 而LinkedBlockingQueue内部则是基于锁,并提供了BlockingQueue的等待性方法。
java.util.concurrent包提供的容器从命名上可以大概分为Concurrent、CopyOnWrite和Blocking三类,同样是线程安全容器,可以简单认为:
- Concurrent类型容器没有CopyOnWrite之类容器相对较重的修改开销。但是,凡事都是有代价的,Concurrent往往提供了较低的遍历一致性。或称之为“弱一致性”,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历。
- 与弱一致性对应的,就是其它同步容器常见的行为“fast-fail”,也就是检测到容器在遍历过程中发生了修改,则抛出ConcurrentModificationException,不再继续遍历。
- 弱一致性的另外一个体现是,size等操作准确性是有限的,未必是百分百准确。与此同时,读取的性能具有一定的不确定性。
线程安全队列一览
有两个特别的实现,ConcurrentLinkedDeque 和 LinkedBlockingDeque。Deque 的侧重点是支持对队列头尾都进行插入和删除,所以提供了特定的方法,如:
- 尾部插入时需要的addLast(e)、offerLast(e)
- 尾部删除时需要的removeLast()、pollLast()
从行为特征来看,绝大部分Queue都是实现了BlockingQueue接口。在常规队列操作基础上,Blocking意味着其提供了特定的等待性操作,获取(take)时等待元素进队,或者插入(put)时等待队列出现空位。
/**
* 获取并移除队列头结点,如果必要,其会等待直到队列出现元素
* ...
*/
E take() throws InterruptedException;
/**
* 插入元素,如果队列已满,则等待直到队列出现空闲空间
* ...
*/
void put(E e) throws InterruptedException;
另一个BlockingQueue需要注意的点,就是是否有界。
- ArrayBlockingQueue是最典型的有界队列,其内部以final的数组保存数据,数组的大小就决定了队列的边界,所以在创建ArrayBlockingQueue时,都要指定容量。
- LinkedBlockingQueue,容易被误解为无边界,但其实其行为和内部代码都是基于有界的逻辑实现的,只不过如果我们没有在创建队列时就指定容量,那么其容量限制就自动被设置为Integer.MAX_VALUE,几乎没有机会到达边界。
- SynchronousQueue,这是一个非常奇葩的队列实现,每个获取操作都要等待插入操作,反之每个插入操作也都要等待获取动作。这个队列的内部容量是0。
- PriorityBlockingQueue是无边界的优先队列,虽然严格意义上来讲,其大小总归是要受到系统资源影响。
- DelayQueue和LinkedTransferQueue同样是无边界的队列。对于无边界的队列,有一个自然的结果,就是put操作永远不会其他BlockingQueue的那种等待情况。
队列选择
以LinkedBlockingQueue、ArrayBlockingQueue和SynchronousQueue为例,一起来分析一下,根据需求可以从这些方面考量:
- 考虑应用场景中对队列边界的要求。ArrayBlockingQueue是有明确的容量限制的,而LinkedBlockingQueue则取决于我们是否在创建时指定,SynchronousQueue则干脆不能缓存任何元素。
- 从空间利用角度,数组结构的ArrayBlockingQueue要比LinkedBlockingQueue紧凑,因为其不需要创建所谓节点,但是其初始分配阶段就需要一段连续的空间,所以初始内存需求更大。
- 通用场景中,LinkedBlockingQueue的吞吐量一般优于ArrayBlockingQueue,因为它实现了更加细粒度的锁操作。
- ArrayBlockingQueue实现比较简单,性能更好预测,属于表现稳定的“选手”。
- 如果我们需要实现的是两个线程之间接力性(handoff)的场景,你可能会选择CountDownLatch,但是SynchronousQueue也是完美符合这种场景的,而且线程间协调和数据传输统一起来,代码更加规范。
- 可能令人意外的是,很多时候SynchronousQueue的性能表现,往往大大超过其它实现,尤其是在队列元素较小的场景。
SynchronousQueue应用场景
/**
* Creates a thread pool that creates new threads as needed, but
* will reuse previously constructed threads when they are
* available, and uses the provided
* ThreadFactory to create new threads when needed.
* @param threadFactory the factory to use when creating new threads
* @return the newly created thread pool
* @throws NullPointerException if threadFactory is null
*/
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
由于ThreadPoolExecutor内部实现任务提交的时候调用的是工作队列(BlockingQueue接口的实现类)的非阻塞式入队列方法(offer方法),因此,在使用SynchronousQueue作为工作队列的前提下,客户端代码向线程池提交任务时,而线程池中又没有空闲的线程能够从SynchronousQueue队列实例中取一个任务,那么相应的offer方法调用就会失败(即任务没有被存入工作队列)。此时,ThreadPoolExecutor会新建一个新的工作者线程用于对这个入队列失败的任务进行处理(假设此时线程池的大小还未达到其最大线程池大小)。
所以,使用SynchronousQueue作为工作队列,工作队列本身并不限制待执行的任务的数量。但此时需要限定线程池的最大大小为一个合理的有限值,而不是Integer.MAX_VALUE,否则可能导致线程池中的工作者线程的数量一直增加到系统资源所无法承受为止。
如果应用程序确实需要比较大的工作队列容量,而又想避免无界工作队列可能导致的问题,不妨考虑SynchronousQueue。SynchronousQueue实现上并不使用缓存空间。
使用SynchronousQueue的目的就是保证“对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务”。
延时队列DelayQueue的使用
DelayQueue 是一个支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。
应用场景:缓存系统(当能够从DelayQueue中获取元素时,说明该缓存已过期);定时任务调度。
public class DelayQueueDemo {
static class Cache implements Runnable {
private boolean stop = false;
private Map<String, String> itemMap = new HashMap<>();
private DelayQueue<CacheItem> delayQueue = new DelayQueue<>();
public Cache() {
// 开启内部线程检测是否过期
new Thread(this).start();
}
/**
* 添加缓存
* @param key
* @param value
* @param exprieTime 过期时间,单位秒
*/
public void put(String key, String value, long exprieTime) {
CacheItem cacheItem = new CacheItem(key, exprieTime);
// 此处忽略添加重复 key 的处理
delayQueue.add(cacheItem);
itemMap.put(key, value);
}
public String get(String key) {
return itemMap.get(key);
}
public void shutdown() {
stop = true;
}
@Override
public void run() {
while (!stop) {
CacheItem cacheItem = delayQueue.poll();
if (cacheItem != null) {
// 元素过期, 从缓存中移除
itemMap.remove(cacheItem.getKey());
System.out.println("key : " + cacheItem.getKey() + " 过期并移除");
}
}
System.out.println("cache stop");
}
}
static class CacheItem implements Delayed {
private String key;
/**
* 过期时间(单位秒)
*/
private long exprieTime;
private long currentTime;
public CacheItem(String key, long exprieTime) {
this.key = key;
this.exprieTime = exprieTime;
this.currentTime = System.currentTimeMillis();
}
@Override
public long getDelay(TimeUnit unit) {
// 计算剩余的过期时间
// 大于 0 说明未过期
return exprieTime - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - currentTime);
}
@Override
public int compareTo(Delayed o) {
// 过期时间长的放置在队列尾部
if (this.getDelay(TimeUnit.MICROSECONDS) > o.getDelay(TimeUnit.MICROSECONDS)) {
return 1;
}
// 过期时间短的放置在队列头
if (this.getDelay(TimeUnit.MICROSECONDS) < o.getDelay(TimeUnit.MICROSECONDS)) {
return -1;
}
return 0;
}
public String getKey() {
return key;
}
}
public static void main(String[] args) throws InterruptedException {
Cache cache = new Cache();
// 添加缓存元素
cache.put("a", "1", 5);
cache.put("b", "2", 4);
cache.put("c", "3", 3);
while (true) {
String a = cache.get("a");
String b = cache.get("b");
String c = cache.get("c");
System.out.println("a : " + a + ", b : " + b + ", c : " + c);
// 元素均过期后退出循环
if (StringUtils.isEmpty(a) && StringUtils.isEmpty(b) && StringUtils.isEmpty(c)) {
break;
}
TimeUnit.MILLISECONDS.sleep(1000);
}
cache.shutdown();
}
}