为类的用户编写线程安全性担保的文档;为类的维护者编写类的同步策略文档。
java.text.SimpleDateFormat并不是线程安全的,如果一个类没有明确指明,就不要假设他是线程安全的。另一方面,倘若不对容器提供的对象(比如HttpSession)的线程安全性做出一些合理的假设,又不可能开发一个基于Servlet的应用。
在设计同步容器返回的迭代器时,并没有考虑到并发修改的问题,当他们察觉容器在迭代开始后被修改,会抛出一个未检测的ConcurrentModificationException。
这个异常也可能出现在单线程的代码中,当对象并非通过iterator.remove,而是被直接从容器中删除时,就会出现这个情况。
在迭代期间,对容器加锁的一个替代办法就是复制容器,因为复制是线程限制(thread-confined)的,没有其他的线程能够在迭代期间对其修改稿。(容器仍然需要在复制期间对自己加锁)
但是要注意,迭代器有时候是隐藏的,字符串的拼接操作是编译转换调用StringBuilder.append(Object)来完成,如果拼接容器,会调用容器的toString方法,标准容器中的toString的实现会通过迭代容器中的每个元素,来获得关于容器内容格式良好的展现。
public class HiddenIterator{
private final Set set=new HashSet();
public void addTenThings(){
Random r=new Random();
for(int i=0;i<10;i++){
add(r.nextInt());
}
System.out.println(“Debug” + set);
}
}
上面的例子中,如果将set包装为synchronizedSet时就不会出现问题。
容器的hashCode和equals方法也会间接调用迭代,比如当容器本身作为一个元素时或者作为另一个容器的key时,类似的,containsAll、removeAll、retainAll方法以及把容器作为参数的构造函数,都会对容器进行迭代。
Java5.0添加了ConcurrentHashMap来替代HashMap实现,当多数操作为读取操作时CopyOnWriteArrayList是List相应的同步实现。新的ConcurrentMap接口加入了对常见复合操作的支持,比如缺少即加入、替换和条件删除。
Java5.0同样添加了两个新的容器类型:Queue和BlockingQueue。JDK提供了几种实现,包括一个传统的FIFO队列ConcurrentLinkedQueue;一个(非并发)具有优先级顺序的队列PriorityQueue。
BlockingQueue扩展了Queue,增加了可阻塞的插入和获取操作,如果队列为空则获取操作被阻塞,如果队列为满则插入操作被阻塞。
Java6加入了ConcurrentSkipListMap和ConcurrentSkipListSet,用来作为同步的SortedMap和SortedSet的并发替代品。
ConcurrentHashMap没有使用一个公共锁同步每一个方法并严格地限制只能有一个线程可以同时访问容器。而是使用了一个更加细化的锁机制,叫分离锁,允许更深层次的访问。提供了不会抛出ConcurrentModificationException的迭代器,因此不需要再容器迭代中加锁。他返回的迭代器具有弱一致性,其可以容许并发修改,当迭代器被创建时,他会遍历已有的元素,并且可以(但是不保证)感应到在迭代器被创建后对容器的修改。
其中size和isEmpty在并发特性上被轻微弱化了。因为size的结果相对于在计算的时候可能已经过期了,他仅仅只是一个估算值,不过事实上size和isEmpty在并发环境下几乎没有什么用处。
只有当你的程序需要在独占访问中加锁时,ConcurrentHashMap才无法胜任。
CopyOnWriteArraySet是同步Set的一个并发替代品。
“写入时复制(copy-on-write)”容器的线程安全性来源于这样一个事实,只要有效的不可变对象被正确发布,那么访问他将不再需要更多的同步。在每次需要修改时,他们会创建并重新发布一个新的容器拷贝以此来实现可变性。
BlockingQueue的实现之一是SynchronousQueue,他根本上不是一个真正的队列,因为他不会为队列元素维护任何存储空间。这类队列只有在消费者充足的时候比较合适。其他的实现有LinkedBlockingQueue和ArrayBlockQueue是FIFO队列,PriorityBlockingQueue是一个按照优先级排序的队列。
桌面搜索应用程序中的生产者和消费者
public class FileCrawler implements Runable{
private final BlockingQueue fileQueue;
private final FileFilter fileFilter;
private final File root;
public void run(){
try{
crawl(root);
}catche(InterruptedException e){
Thread.currentThread().interrupt();
}
}
private void crawl(File root)throws InterruptedException{
File[] entried=root.listFiles(fileFilter);
if(entries!=null){
for(File entry:entries){
if(entry.isDirectory()){
crawl(entry);
}else if(!alreadyIndexed(entry)){
fileQueue.put(entry);
}
}
}
}
}
public class Indexer implements Runable{
private final BlockingQueue queue;
public Indexer(BlockingQueue queue){
this.queue=queue;
}
public void run(){
try{
while(true){
indexFile(queue.take());
}catch(InterruptException e){
Thread.currentThread().interrupt();
}
}
}
}
public static void startIndexing(File[] roots){
BlockingQueue queue=new LinkedBlockingQueue(BOUND);
FileFilter filter = new FileFilter(){
public boolean accpet(File file){
return true;
}
}
for(File root:roots){
new Thread(new FileCrawler(queue,filter,root)).start();
}
for(int i=0;i
Java6新增了两个容器类型,Deque和BlockingDeque,他们分别扩展了Queue和BlockingQueue。Deque是一个双端队列,实现他们的是ArrayDeque和LinkedBlockingDeque。
正如阻塞队列适用于生产者消费者模式一样,双端队列使得他们与一种叫做窃取工作的模式相关联。一个生产者消费者模式中,所有的消费者只共享一个工作队列,在窃取工作的设计中,每一个消费者都有一个自己的双端队列,如果一个消费者完成了自己的全部工作,他可以偷取其他消费者的双端队列的末尾任务。这样可以保证每一个线程都保持忙碌的状态。