同步容器将所有对容器状态的访问都串行化,以实现线程安全性。这种方式的缺点是严重降低并发性。Java 5.0提供了多种并发容器来改进同步容器的性能。如ConcurrentHashMap代替同步且基于散列的Map,CopyOnWriteArrayList,用于在遍历操作主要操作的情况下代替同步的List,同时提供了一些常见的符合操作,如,若没有则添加,替换及有条件删除等。Java 6引入了ConcurrentSckipListMap和ConcurrentSkipListSet,分别代替同步的SortedMap和SortedSet。
通过并发容器来代替同步容器,可以极大地提高性能并降低风险。
ConcurrentHashMap
同步容器类在执行每个操作期间都持有锁。在一些操作,如HashMap.get和List.contains,可能包含大量工作:遍历散列表或链表来查找特定的对象,必须调用equals方法。这将花费大量时间,而其他线程在这段时间将不能访问该容器。
ConcurrentHashMap 也是基于散列的Map,并不是将每个方法都在同一个锁上同步并使每次只有一个线程访问容器,而是使用一种粒度更细的加锁机制来实现各大程度的共享,这种机制称为分段锁。任意数量的读取线程可以并发的访问Map,执行读取操作的线程和执行写入操作的线程可以并发的访问Map,并且一定数量的写入线程可以并发的修改Map。
类似的并发容器增强了同步容器,它们提供的迭代器不会抛出ConcurrentModeficationException,因此不需要在迭代过程加锁。 ConcurrentHashMap 返回的迭代器具有弱一致性,可以容忍并发的修改,当创建迭代器时会遍历已有的元素,并可以在迭代器被构造后将修改操作反映给容器。
尽管并发容器改进很多,但是还存在需要权衡的因素,对于需要在整个map上计算的方法,如size和isEmpty,这些方法返回的结果可能已经过期了,实际上只是估计值。虽然这个看上去存在很大问题,实际上在并发环境下,这些方法作用很小,因为他们的值一直在变。这些操作的弱化换取其他更重要操作的性能优化,如get,put,containsKey和remove等。
与Hashtable和synchronizedMap相比, ConcurrentHashMap有着更多的优势及更少的劣势,因此在大多数情况下使用 ConcurrentHashMap代替同步Map能进一步提高代码的可伸缩性。只有需要加锁Map进行独占访问时才应该放弃 ConcurrentHashMap。
由于 ConcurrentHashMap不能被加锁来执行独占访问,因此无法使用客户端加锁建立新的原子操作,但是一些常见的复合操作已经实现为原子操作。
CopyOnWriteArrayList
CopyOnWriteArrayList 用于替代同步List,在某些情况下提供更好的并发性能。并且迭代期间不需要加锁或者复制。线程安全性在于,“写入时复”制,每次修改时都会创建并重新发布一个新的容器副本。“写入时复制”容器的迭代器保留一个指向底层基础数组的引用,这个数组位于迭代器的起始位置,由于不会被修改,因此同步时只需确保数组内容的可见性。多个线程可以对这个容器进行迭代。容器的迭代器返回的元素与迭代器创建时的元素完全一致。
每当修改容器时都会复制底层数组,这需要一定的开销,特别是规模较大时。仅当迭代操作远大于修改操作时,才应该使用"写入时复制"容器。类似的还有CopyOnWriteArraySet。如用于事件通知系统,注册和注销事件的操作远少于接受事件的操作。