并发集合(一)
----------
我们将探讨集合框架中新的Queue接口、这个接口的非并发和并发实现、并发Map实现和专用于读操作大大超过写操作这种情况的并发List和Set实现。
队列Queue与BlockingQueue
java.util包为集合提供了一个新的基本接口:java.util.Queue。虽然肯定可以在相对应的两端进行添加和删除而将java.util.List作为队列对待,但是这个新的Queue接口提供了支持添加、删除和检查集合的更多方法。
Queue继承自collection。除了基本的Collection操作外,队列还提供其他的插入、提取和检查操作。
每个方法都存在两种形式:一种抛出异常(操作失败时),另一种返回一个特殊值(null或false,具体取决于操作)。插入操作的后一种形式是用于专门为有容量限制的Queue实现设计的;在大多数实现中,插入操作不会失败。队列通常(但并非一定)以FIFO(先进先出)的方式排序各个元素。不过优先级队列和LIFO队列(或堆栈)例外,前者根据提供的比较器或元素的自然顺序对元素进行排序,后者按LIFO(后进先出)的方式对元素进行排序。无论使用哪种排序方式,
队列的头都是调用remove()或poll()所移除的元素。
在FIFO队列中,所有的新元素都插入队列的末尾。其他种类的队列可能使用不同的元素放置规则。每个Queue实现必须指定其顺序属性。
如果可能,
offer方法可插入一个元素,否则返回false。这与Collection.add方法不同,该方法只能通过抛出未经检查的异常使添加元素失败。offer方法设计用于正常的失败情况,而不是出现异常的情况,例如在容量固定(有界)的队列中。 remove()和poll()方法可移除和返回队列的头。到底从队列中移除哪个元素是队列排序策略的功能,而该策略在各种实现中是不同的。remove()和poll()方法仅在队列为空时其行为有所不同:remove()方法抛出一个异常,而
poll()方法则返回null。 element()和peek()返回,但不移除,队列的头。
Queue接口并未定义阻塞队列的方法,而这在并发编程中是很常见的。BlockingQueue接口定义了那些等待元素出现或等待队列中有可用空间的方法,这些方法扩展了此接口。
Queue实现通常不允许插入null元素,尽管某些实现(如LinkedList)并不禁止插入null。即使在允许null的实现中,也不应该将null插入到Queue中,因为null也用作 poll方法的一个特殊返回值,表明队列不包含元素。 Queue实现通常未定义equals和hashCode方法的基于元素的版本,而是从Object类继承了基于身份的版本,因为对于具有相同元素但有不同排序属性的队列而言,基于元素的相等性并非总是定义良好的。
boolean add(Object e) |
|
public boolean offer(Object element) |
|
public Object remove() |
|
public Object poll() |
|
public Object element() |
|
public Object peek() |
|
在JDK中有两组Queue实现:实现了新BlockingQueue接口的和没有实现这个接口的。我将首先分析那些没有实现的。在最简单的情况下,原来有的java.util.LinkedList实现已经改造成不仅实现java.util.List接口,而且还实现java.util.Queue接口。可以将LinkedList集合看成这两者中的任何一种。下面的程序将显示把LinkedList作为Queue的使用方法:
package queuedemo;
import java.util.LinkedList;
import java.util.Queue;
public class QueueTest{
public static void main(String[] args){
Queue queue = new LinkedList();
queue.offer("One");
queue.offer("Two");
queue.offer("Three");
queue.offer("Four");
System.out.println("Head of queue is: " + queue.poll());
}
}
输出结果为: Head of queue is : One
PriorityQueue和ConcurrentLinkedQueue类在Collection Framework中加入两个具体集合实现。PriorityQueue类实质上维护了一个有序列表。加入到Queue中的元素根据它们的天然排序(通过其java.util.Comparable实现)或者根据传递给构造函数的java.util.Comparator实现来定位。将上面程序中的LinkedList改变为PriorityQueue将会打印出Four而不是One,因为按字母排列,即字符串的天然顺序,Four是第一个。ConcurrentLinkedQueue是基于链接节点的、线程安全的队列。并发访问不需要同步。因为它在队列的尾部
添加元素并从头部删除它们,所以只要不需要知道队列的大小,ConcurrentLinkedQueue对公共集合的共享访问就可以工作得很好。收集关于队列大小的信息会很慢,需要遍历队列。
package queuedemo;
import java.util.PriorityQueue;
import java.util.Queue;
public class PriorityQueueDemo{
public static void main(String[] args){
Queue<String> queue = new PriorityQueue<String>();
queue.offer("One");
queue.offer("Two");
queue.offer("Three");
queue.offer("Four");
System.out.println("Head of queue is: " + queue.poll());
}
}
输出结果如下: Head of queue is: Four
新的java.util.concurrent包可用的具体集合类中加入了BlockingQueue接口和五个阻塞队列类。阻塞队列实质上就是一种带有一点扭曲的FIFO数据结构,不是立即从队列中添加或者删除元素,线程执行操作被阻塞,直到有空间或者元素可用。BlockingQueue接口的javadoc给出了阻塞队列的基本用法,生产者中的put()操作会在没有空间可用时阻塞,而消费者的take()操作会在队列中没有任何东西时阻塞。
五个队列所提供的各有不同:
ArrayBlockingQueue |
|
LinkedBlockingQueue |
|
PriorityBlockingQueue |
|
DelayQueue |
|
SynchronousQueue |
|
下面以ArrayBlockingQueue为例写一个程序,表示生产者--消费者问题。生产者向阻塞队列中放入字符,消费者从阻塞队列中移除字符。
package queuedemo;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo{
public static void main(String[] args){
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(5);
Producer p = new Producer(queue);
Consumer c1 = new Consumer(queue);
Consumer c2 = new Consumer(queue);
new Thread(p).start();
new Thread(c1).start();
new Thread(c2).start();
}
}
class Producer implements Runnable{
private final BlockingQueue<String> queue;
Producer(BlockingQueue<String> q){
queue = q;
}
public void run(){
try{
for(int i=0;i<100;i++){
queue.put(produce());
}
}catch(InterruptedException ex){
ex.printStackTrace();
}
}
String produce(){
String temp = ""+(char)('A'+(int)(Math.random()*26));
System.out.println("produce " +temp);
return temp;
}
}
class Consumer implements Runnable{
private final BlockingQueue<String> queue;
Consumer(BlockingQueue<String> q){
queue = q;
}
public void run(){
try{
for(int i=0;i<100;i++){
consume(queue.take());
}
}catch(InterruptedException ex){
ex.printStackTrace();
}
}
void consume(String x){
System.out.println("consume " + x);
}
}
输出结果如下:
produce W
produce S
produce D
produce Q
consume S
consume W
consume Q
consume D
produce V
produce J
......
前两个类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))。因为队列的大小没有界限,使得添加可以立即返回,但是在延迟时间过去之前,不能从队列中取出元素。如果多个元素完成了延迟,那么最早失效/失效时间最长的元素将第一个取出。实际上没有听上去这样复杂。下面的程序演示了这种新的阻塞队列集合的使用:
package queuedemo;
import java.util.Random;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class DelayQueueDemo{
static class NanoDelay implements Delayed{
long trigger;
NanoDelay(long i){
trigger = System.nanoTime() + i;
}
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 = tringger = System.nanoTime();
return unit.convert(n,TimeUnit.NANOSECONDS);
}
public long getTriggerTime(){
return trigger;
}
public String toString(){
return String.valueOf(trigger);
}
@Override
public int compareTo(Delayed o){
long i = tringger;
long j = ((NanoDelay)o).trigger;
if(i<j) return -1;
if(i>j) return 1;
return 0;
}
}
public static void main(String[] args)throws InterruptedException{
Random random = new Random();
DelayQueue<NanoDelay> queue = new DelayQueue<NanoDelay>();
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;
}
}
}
运行结果如下:
Trigger time: 5629057839457
Trigger time: 5629057894502
Delta: 55045
Trigger time:5629057925948
Delat:31446
......
这个例子首先是一个内部类NanoDelay,它实质上将暂停任意纳秒(nanosecond)数,这里利用了System的新nanoTime()方法。然后main()方法只是将NanoDelay对象放到队列中并再次将它们取出来。如果希望队列项做一些其他事情,就需要在Delayed对象的实现中加入方法,并在从队列中取出后调用这个新方法。显示从队列中取出元素的两次调用之间的时间差。如果时间差是负数,可以视为一个错误,因为永远不会在延迟时间结束时,在一个更早的触发时间从队列中取得项。
SynchronousQueue类是最简单的。它没有内部容量。它就像线程之间的手递手机制。在队列中加入一个元素的生产者会等待另一个线程的消费者。当这个消费者出现时,这个元素就直接在消费者和生产者之间传递,永远不会加入到阻塞队列中。