并发集合(一)
----------
我们将探讨集合框架中新的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)

将指定的元素插入此队列(如果立即可行且不会违反容量限制),在成功时返回true,

如果当前没有可用的空间,则抛出IllegalStateException。


public boolean offer(Object element)

将指定的元素插入此队列(如果立即可行且不会违反容量限制),当使用有容量限制的队列时,

此方法通常要优于add(E),后者可能无法插入元素,而只是抛出一个异常。


public Object remove()

获取并移除此队列的头

public Object poll()

获取并移除此队列的头,如果此队列为空,则返回null

public Object element()

获取但是不移除此列队的头。此队列为空时将抛出一个异常

public Object peek()

获取但不移除此队列的头,如果此队列为空,则返回null

在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

一个利用BlockingQueue接口的简单聚集机制

下面以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类是最简单的。它没有内部容量。它就像线程之间的手递手机制。在队列中加入一个元素的生产者会等待另一个线程的消费者。当这个消费者出现时,这个元素就直接在消费者和生产者之间传递,永远不会加入到阻塞队列中。