无论是直接迭代还是使用 for-each
,对容器类进行迭代的标准方式都是使用 Iterator
。
但是,如果有其他线程并发地修改容器,那么即使是使用迭代器也无法避免在迭代期间对容器加锁。
在设计同步容器类的迭代器时并没有考虑到并发修改的问题,并且它们表现出的行为是 "及时失败" (fail-fast) 的。
也就是说,当它们发现容器在迭代过程中被修改时,就会抛出 ConcurrentModificationException
异常。
这种及时失败的迭代器并不是一种完备的处理机制,而只是捕获异常,因此只能作为并发问题的预警指示器。
它们采用的实现方式是:将计数器的变化与容器关联起来,如果在迭代期间计数器被修改,那么 hasNext
或 next
将抛出 ConcurrentModificationException
。
然而,这种检查是在没有同步的情况下进行的,因此可能会看到失效的计数值,迭代器并没有意识到已经发生了修改。这是一种设计上的权衡,从而降低并发修改操作的检测代码对程序性能带来的影响。
当对象直接从容器中删除而不是通过
Iterator.remove
来删除时,即使是在单线程的程序中也会抛出ConcurrentModificationException
要想避免出现 ConcurrentModificationException
,就必须在迭代过程中持有容器的锁或者克隆容器,在副本上进行迭代。
容器的 toString
、 hashCode
和 equals
等方法会间接的执行迭代操作,当容器作为另一个容器的元素或键值时,就会出现这种情况。
containsAll
、removeAll
和 retainAll
等方法,以及把容器作为参数的构造函数,都会对容器进行迭代。
所有这些简介的迭代操作都可能抛出 ConcurrentModificatcionException
。
ConcurrentHashMap
用来替代同步基于散列的 Map
。
CopyOnWriteArrayList
用于在遍历操作为主要操作的情况下替代同步的 List
。
Queue
用来临时保存一组等待处理的元素,基于 LinkedList
实现。它提供了几种实现:ConcurrentLinkedQueue
是一个先进先出队列,PriorityQueue
(非并发) 是一个优先队列。
Queue
上的操作不会阻塞,如果队列为空,那么获取元素的操作将返回空值。
BlockingQueue
扩展了 Queue
,增加了可阻塞的插入和获取等操作。如果队列为空,那么获取元素的操作将会一直阻塞,直到队列中出现一个可用的元素。如果队列已满 (有界队列),那么插入元素的操作将一直阻塞,直到队列中出现可用的空间。
ConcurrentHashMap
使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为分段锁。
Java 8 之前的实现机制
ConcurrentHashMap
返回的迭代器具有弱一致性,而并非及时失败。弱一致性的迭代器可以容忍并发的修改,当创建迭代器时会遍历已有的元素,并可以 (但是不保证) 在迭代器被构造后将修改操作反应给容器。
ConcurrentMap
接口中的原子操作:
public interface ConcurrentMap<K,V> extends Map<K,V> {
// 仅当 K 没有相应的映射值时才插入
V putIfAbsent(K key, V value);
// 仅当 K 被映射到 V 时才移除
boolean remove(K key, V value);
// 仅当 K 被映射到 oldValue 时才替换为 newValue
boolean replace(K key, V oldValue, V newValue);
// 仅当 K 被映射到某个值时才替换为 newValue
V replace(K key, V newValue);
}
CopyOnWriteArrayList
用于替代同步 List
,并且在迭代期间不需要对容器进行加锁或复制。
类似地,CopyOnWriteArraySet
的作用是替代同步 Set
。
"写入时复制 (Copy-On-Write)" 容器的线程安全性在于,只要正确地发布一个事实不可变的对象,那么在访问该对象时就不再需要进一步的同步。
在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。
"写入时复制" 容器的迭代器保留一个指向底层基础数组的引用,这个数组位于迭代器的起始位置,由于它不会被修改,因此在对其进行同步时只需确保数组内容的可见性。
"写入时复制" 容器返回的迭代器不会抛出 ConcurrentModificationException
,并且返回的元素与迭代器创建时的元素完全一致,而不必考虑之后修改操作所带来的影响。
每当修改容器时都会复制底层数组,需要一定的开销,特别是当容器的规模较大时。
仅当迭代操作远远多于修改操作时,才应该使用 "写入时复制" 容器。
阻塞队列提供了可阻塞的 put
和 take
方法,以及支持定时的 offer
和 poll
方法。
如果队列已经满了,那么 put
方法将阻塞直到有空间可用。
如果队列为空,那么 take
方法将会阻塞直到有元素可用。
BlockingQueue
有多种实现,其中,LinkedBlockingQueue
和 ArrayBlockingQueue
是 FIFO
队列。
PriorityBlockingQueue
是一个按优先级排序的队列,可以按照某种顺序而不是 FIFO
来处理元素。既可以根据元素的自然顺序来比较元素 (如果它们实现了 Comparable
方法),也可以使用 Comparator
来比较。
SynchronousQueue
并不是一个真正的队列,因为它不会为队列中的元素维护存储空间。与其它队列不同的是,它维护一组线程,这些线程在等待着把元素加入或者移出队列。因为 SynchronousQueue
没有存储功能,因此 put
和 take
会一直阻塞,直到有另一个线程已经准备好参与到交付过程中。仅当有足够多的消费者,并且总是有一个消费者准备好获取交付的工作时,才适合使用该队列。
闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。
闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。
当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。
闭锁可以用来确保某些活动直到其它活动都完成后才继续执行。
CountDownLatch
可以使一个或多个线程等待一组事件发生。