超越 Map、Collection、List 和 Set的Queue
一、版权说明:
本文部分文字摘自John Zukowski的《驯服 Tiger: 并发集合》(http://www-900.ibm.com/developerWorks/cn/java/j-tiger06164/index.shtml?ca=dwcn-newsletter-java)
二、内容简介:
Doug Lea 最初编写的 util.concurrent 包变成了 JSR-166 ,然后又变成了 J2SE 平台的 Tiger 版本。这个新库提供的是并发程序中通常需要的一组实用程序。如果对于优化对集合的多线程访问有兴趣,那么您就找对地方了。
在 Java 编程的早期阶段,位于 Oswego 市的纽约州立大学(SUNY) 的一位教授决定创建一个简单的库,以帮助开发人员构建可以更好地处理多线程情况的应用程序。这并不是说用现有的库就不能实现,但是就像有了标准网络库一样,用经过调试的、可信任的库更容易自己处理多线程。在 Addision-Wesley 的一本相关书籍的帮助下,这个库变得越来越流行了。最终,作者 Doug Lea 决定设法让它成为 Java 平台的标准部分 —— JSR-166。
三、介绍 Queue 接口:
java.util 包为集合提供了一个新的基本接口:java.util.Queue。虽然肯定可以在相对应的两端进行添加和删除而将 java.util.List 作为队列对待,但是这个新的 Queue 接口提供了支持添加、删除和检查集合的更多方法,如下所示:
public boolean offer(Object element) public Object remove() public Object poll() public Object element() public Object peek() |
Queue接口被设计为一个存放对象成员的集合。作为基本的Collection操作,Queue提供了一些新的添加、删除、检查操作。以下为它们的方法名和规则列表:
操作 | 方法名 | 规则 |
添加 | offer(x) | 尝试添加,成功返回true,不成功返回false |
add(x) | 不成功抛出异常 | |
删除 | poll() | 尝试删除,成功返回true,不成功返回false |
remove() | 不成功抛出异常 | |
检查 | peek() | 尝试返回Queue头的成员,如果Queue为null,则返回null |
element() | 返回Queue头的成员,如果Queue为null,则抛出异常 |
四、Queue工作原理:
Queue的成员排序:Queue通常按照FIFO的方式排列成员,但不是必需的。比如优先级队列(priority queues)就是例外的队列,它按照比较器(comparator)来排列队列成员;还有按照队列成员本来的顺序排列;LIFO队列(后进先出队列)按照后进先出的顺序排列队列成员。无论使用哪种排列顺序,队列头的成员可以使用remove()或poll()方法删除。在FIFO队列中,新成员被添加到队列的尾部。其它种类的队列不能使用不同的存放方式。每种Queue实现必须提供它自己的排列属性。
offer方法添加一个成员,如果成功返回true;如果失败返回false。这与Collection.add方法不同之处在于:Collection.add遇到失败的情况将抛出unchecked异常。所以offer方法被设计用在视失败为正常的情况下,而不是发生异常,例如一个固定长度的Queue。
remove()和poll()方法删除对列头部的成员。至于哪个成员被删除则完全按照队列的排序规则,这依照实现各有不同。remove()和poll()方法的行为只有在队列为空情况下会有所不同:poll()方法返回null,而remove()方法将抛出异常。
element()和peek()方法只返回队列头的成员,而不删除任何成员。
Queue接口没有定义阻塞队列(blocking queue)的方法,而这个方法是并发编程时通常要用到的。阻塞队列这个方法将使成员等待知道队列中有空间,此方法被定义在edu.emory.mathcs.backport.java.util.concurrent.BlockingQueue接口中(一个扩展了Queue接口的接口)J。
Queue接口的实现通常不允许插入null成员到队列中,尽管一些例如LinkedList的实现不限制插入null。甚至在一些允许插入null的Queue接口的实现中,null不允许插入到Quqeue中,而且当队列不包含任何成员时poll()方法会返回null。
Queue接口的实现通常不定义基于成员的equals和hashCode方法,但从Object类继承了标识类的一些方法。因为在不同排序的队列中基于成员的equals不能很好的被定义。
使用基本队列
在 Tiger 中有两组 Queue 实现:实现了新 BlockingQueue 接口的和没有实现这个接口的。我将首先分析那些没有实现的。
在最简单的情况下,原来有的 java.util.LinkedList 实现已经改造成不仅实现 java.util.List 接口,而且还实现 java.util.Queue 接口。可以将集合看成这两者中的任何一种。清单 1 显示将 LinkedList 作为 Queue 使用的一种方法:
Queue queue = new LinkedList(); queue.offer("One"); queue.offer("Two"); queue.offer("Three"); queue.offer("Four"); // Head of queue should be One System.out.println("Head of queue is: " + queue.poll()); |
再复杂一点的是新的 java.util.AbstractQueue 类。这个类的工作方式类似于 java.util.AbstractList 和 java.util.AbstractSet 类。在创建自定义集合时,不用自己实现整个接口,只是继承抽象实现并填入细节。使用 AbstractQueue 时,必须为方法 offer()、 poll() 和 peek() 提供实现。像 add() 和 addAll() 这样的方法修改为使用 offer(),而 clear() 和 remove() 使用 poll()。最后,element() 使用 peek()。当然可以在子类中提供这些方法的优化实现,但是不是必须这么做。而且,不必创建自己的子类,可以使用几个内置的实现,其中两个是不阻塞队列: PriorityQueue 和 ConcurrentLinkedQueue。
PriorityQueue 和 ConcurrentLinkedQueue 类在 Collection Framework 中加入两个具体集合实现。PriorityQueue 类实质上维护了一个有序列表。加入到 Queue 中的元素根据它们的天然排序(通过其java.util.Comparable 实现)或者根据传递给构造函数的 java.util.Comparator 实现来定位。将清单 2 中的 LinkedList 改变为 PriorityQueue 将会打印出 Four 而不是 One,因为按字母排列 —— 字符串的天然顺序 —— Four 是第一个。ConcurrentLinkedQueue 是基于链接节点的、线程安全的队列。并发访问不需要同步。因为它在队列的尾部添加元素并从头部删除它们,所以只要不需要知道队列的大小,ConcurrentLinkedQueue 对公共集合的共享访问就可以工作得很好。收集关于队列大小的信息会很慢,需要遍历队列。
使用阻塞队列
新的 java.util.concurrent 包在 Collection Framework 中可用的具体集合类中加入了 BlockingQueue 接口和五个阻塞队列类。假如不熟悉阻塞队列概念,它实质上就是一种带有一点扭曲的 FIFO 数据结构。不是立即从队列中添加或者删除元素,线程执行操作阻塞,直到有空间或者元素可用。BlockingQueue 接口的 Javadoc 给出了阻塞队列的基本用法,如清单 2 所示。生产者中的 put() 操作会在没有空间可用时阻塞,而消费者的 take() 操作会在队列中没有任何东西时阻塞。
class Producer implements Runnable { private final BlockingQueue queue; Producer(BlockingQueue q) { queue = q; } public void run() { try { while(true) { queue.put(produce()); } } catch (InterruptedException ex) { ... handle ...} } Object produce() { ... } }
class Consumer implements Runnable { private final BlockingQueue queue; Consumer(BlockingQueue q) { queue = q; } public void run() { try { while(true) { consume(queue.take()); } } catch (InterruptedException ex) { ... handle ...} } void consume(Object x) { ... } }
class Setup { void main() { BlockingQueue q = new SomeQueueImplementation(); Producer p = new Producer(q); Consumer c1 = new Consumer(q); Consumer c2 = new Consumer(q); new Thread(p).start(); new Thread(c1).start(); new Thread(c2).start(); } } |
五个队列所提供的各有不同:
- ArrayBlockingQueue:一个由数组支持的有界队列。
- LinkedBlockingQueue:一个由链接节点支持的可选有界队列。
- PriorityBlockingQueue:一个由优先级堆支持的无界优先级队列。
- DelayQueue:一个由优先级堆支持的、基于时间的调度队列。
- SynchronousQueue:一个利用 BlockingQueue 接口的简单聚集(rendezvous)机制。
前两个类 ArrayBlockingQueue 和 LinkedBlockingQueue 几乎相同,只是在后备存储器方面有所不同,LinkedBlockingQueue 并不总是有容量界限。无大小界限的 LinkedBlockingQueue 类在添加元素时永远不会有阻塞队列的等待(至少在其中有 Integer.MAX_VALUE 元素之前不会)。
PriorityBlockingQueue 是具有无界限容量的队列,它利用所包含元素的 Comparable 排序顺序来以逻辑顺序维护元素。可以将它看作 TreeSet 的可能替代物。例如,在队列中加入字符串 One、Two、Three 和 Four 会导致 Four 被第一个取出来。对于没有天然顺序的元素,可以为构造函数提供一个 Comparator 。不过对 PriorityBlockingQueue 有一个技巧。从 iterator() 返回的 Iterator 实例不需要以优先级顺序返回元素。如果必须以优先级顺序遍历所有元素,那么让它们都通过 toArray() 方法并自己对它们排序,像 Arrays.sort(pq.toArray())。
新的 DelayQueue 实现可能是其中最有意思(也是最复杂)的一个。加入到队列中的元素必须实现新的 Delayed 接口(只有一个方法 —— long getDelay(java.util.concurrent.TimeUnit unit))。因为队列的大小没有界限,使得添加可以立即返回,但是在延迟时间过去之前,不能从队列中取出元素。如果多个元素完成了延迟,那么最早失效/失效时间最长的元素将第一个取出。实际上没有听上去这样复杂。清单 3 演示了这种新的阻塞队列集合的使用:
import edu.emory.mathcs.backport..*; import edu.emory.mathcs.backport..concurrent.*;
public class Delay { /** * Delayed implementation that actually delays */ static class NanoDelay implements Delayed { long trigger; NanoDelay(long i) { trigger = Utils.nanoTime() + i; } public int compareTo(Object y) { long i = trigger; long j = ((NanoDelay)y).trigger; if (i < j) return -1; if (i > j) return 1; return 0; } public boolean equals(Object other) { return ((NanoDelay)other).trigger == trigger; } public boolean equals(NanoDelay other) { return ((NanoDelay)other).trigger == trigger; } public long getDelay(TimeUnit unit) { long n = trigger - Utils.nanoTime(); return TimeUnit.NANOSECONDS.convert(n,unit); } public long getTriggerTime() { return trigger; } public String toString() { return String.valueOf(trigger); } } public static void main(String args[]) throws InterruptedException { Random random = new Random(); DelayQueue queue = new DelayQueue(); for (int i=0; i < 5; i++) { queue.add(new NanoDelay(random.nextInt(1000))); } long last = 0; for (int i=0; i < 5; i++) { NanoDelay delay = (NanoDelay)(queue.take()); long tt = delay.getTriggerTime(); System.out.println("Trigger time: " + tt); if (i != 0) { System.out.println("Delta: " + (tt - last)); } last = tt; } } } |
这个例子首先是一个内部类 NanoDelay,它实质上将暂停给定的任意纳秒(nanosecond)数,这里利用了edu.emory.mathcs.backport.java.util.Utils的 nanoTime() 方法。然后 main() 方法只是将 NanoDelay 对象放到队列中并再次将它们取出来。如果希望队列项做一些其他事情,就需要在 Delayed 对象的实现中加入方法,并在从队列中取出后调用这个新方法。(请随意扩展 NanoDelay 以试验加入其他方法做一些有趣的事情。)显示从队列中取出元素的两次调用之间的时间差。如果时间差是负数,可以视为一个错误,因为永远不会在延迟时间结束后,在一个更早的触发时间从队列中取得项。
SynchronousQueue 类是最简单的。它没有内部容量。它就像线程之间的手递手机制。在队列中加入一个元素的生产者会等待另一个线程的消费者。当这个消费者出现时,这个元素就直接在消费者和生产者之间传递,永远不会加入到阻塞队列中。
清单4:使用DelayQueue实现生产者/消费者
package bjInfoTech.util.threadManage.concurrent.tryIt; /* * DelayQueue_test.java * * Created on 2005年1月11日, 下午4:23 */ import edu.emory.mathcs.backport.java.util.concurrent.*; import java.util.*;
/** * 测试util.concurrent包的jsdk1.4版-backport-util-concurrent---DelayQueue * @author 聪明的猪 */ public class DelayQueue_test {
private static DelayQueue Q=new DelayQueue();
/** * 生产者:向队列中放入对象 */ private static class Producer implements Runnable { public void run() { while (true) { try { Q.put(new Object()); Thread.yield(); } catch (Exception e) { Thread.currentThread().interrupt(); } } } }
/** * 消费者:从队列中取出50000个信息 */ private static class Consumer implements Runnable { private static final int MAX = 50000; /** * * @throws cassCastException 当生产者调用drainto方法处理传入object对象时,将发生该错误。 */ public void run() { long t1 = System.currentTimeMillis(); List list = new ArrayList(); int i = 0; while (i < MAX) { //i += Q.drainTo(list); //list.clear(); i+=Q.size(); Q.clear(); Thread.yield(); } long t2 = System.currentTimeMillis(); System.out.println("time = " + (t2 - t1) + "ms"); } }
public static void main(String[] args) { Thread c = new Thread(new Consumer()); c.start();
Thread p = new Thread(new Producer()); p.setDaemon(true); p.start(); }
} |