本文整理自《Java并发编程实战》一书。
Java5.0 提供了多种并发容器类来改进同步容器的性能。同步容器将所有对容器状态的访问都串行化,以实现他们的线程安全性。这种方法的代价是严重降低并发性,当多个线程竞争容器的锁时,吞吐量将严重降低。
另一方面,并发容器是针对多个线程并发访问设计的。在Java5.0中增加了ConcurrentHashMap,用来替代同步且基于散列的Map,以及CopyOnWriteArrayList,用于在遍历操作为主要操作的情况下代替同步的List。在新的ConcurrentMap接口中增加了对一些常见复合操作的支持,例如“若没有则添加putIfAbsent”、“替换replace”以及“有条件删除remove”等。
通过并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。
Java5.0增加了两种新的容器类型:Queue和BlockingQueue。Queue用来临时保存一组等待处理的元素。它提供了几种实现,包括:ConcurrentLinkedQueue,这是一个传统的先进先出FIFO队列,以及PriorityQueue,这是一个(非并发的)优先队列。BlockingQueue扩展了Queue,增加了可阻塞的插入put 和获取take 等操作,如果队列为空,那么获取元素的操作会一直阻塞,直到队列中出现一个可用的元素。如果队列已满(对于有界队列来说),那么插入元素的操作将一直阻塞,直到队列中出现可用的空间。在“生产者-消费者”这种设计模式中,阻塞队列是非常有用的。
正如ConcurrentHashMap用于代替基于散列的同步Map,Java6也引进了ConcurrentSkipListMap和ConcurrentSkipListSet,分别作为同步的SortedMap和SortedSet的并发替代品(例如用synchronizedMap包装的TreeMap或TreeSet)。
同步容器类在执行每个操作期间都持有同一个锁,在一些操作,如HashMap.get 和 List.contains,当遍历散列桶或者链表来查找某个特定对象时,必须在许多元素上调用equals(而equals本身还包含一定的计算量),此时,其他线程在这段时间内都不能并发访问该容器。ConcurrentHashMap也是一个基于散列的Map,但它使用了完全不同的加锁策略来提供更高的并发性和伸缩性,这种加锁称为分段锁Lock Striping,这种机制中,任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map。
ConcurrentHashMap与其他并发容器一起增强了同步容器类:它们提供的迭代器不会抛出ConcurrentModificationException,因此不需要在迭代过程中对容器加锁。ConcurrentHashMap返回的迭代器具有弱一致性(Weakly Consistent),而并非“及时失败”。
尽管有这些改进,但仍然有一些需要权衡的因素。对于一些需要在整个Map上进行的计算方法,如size和isEmpty,这些方法的语义被略微减弱以反映容器的并发特性。允许size返回一个近似值而不是一个精确值,这看上去令人不安,但事实上size和isEmpty这样的方法在并发环境下用处很小,因为它们的返回值总在不断变化。因此,这些操作的需求都被弱化了,以换取对其他更重要操作的性能优化,包括get、put、containsKey和remove等。
与Hashtable和synchronizedMap相比,ConcurrentHashMap有着更多的优势以及更少的劣势,因此在大多数情况下,用ConcurrentHashMap来替代同步Map能进一步提高代码的可伸缩性。只有当应用程序需要加锁Map以进行独占访问时,才应该放弃使用ConcurrentHashMap。
ConcurrentMap接口提供了额外的原子Map操作:putIfAbsent、remove、replace、overload replace。CopyOnWriteArrayList、CopyOnWriteArraySet等 “写入时复制(Copy-On-Write)”容器,多个线程可以同时对这个容器进行迭代,而不会彼此干扰或者与修改容器的线程相互干扰,且“写入时复制”容器返回的迭代器不会抛出ConcurrentModificationException。