Skip to content

Latest commit

 

History

History
111 lines (60 loc) · 6.55 KB

05第五章:基础构建模块.md

File metadata and controls

111 lines (60 loc) · 6.55 KB

5.1.2 迭代器与 ConcurrentModificationException

无论是直接迭代还是使用 for-each,对容器类进行迭代的标准方式都是使用 Iterator

但是,如果有其他线程并发地修改容器,那么即使是使用迭代器也无法避免在迭代期间对容器加锁。

在设计同步容器类的迭代器时并没有考虑到并发修改的问题,并且它们表现出的行为是 "及时失败" (fail-fast) 的。

也就是说,当它们发现容器在迭代过程中被修改时,就会抛出 ConcurrentModificationException 异常。

这种及时失败的迭代器并不是一种完备的处理机制,而只是捕获异常,因此只能作为并发问题的预警指示器。

它们采用的实现方式是:将计数器的变化与容器关联起来,如果在迭代期间计数器被修改,那么 hasNextnext 将抛出 ConcurrentModificationException

然而,这种检查是在没有同步的情况下进行的,因此可能会看到失效的计数值,迭代器并没有意识到已经发生了修改。这是一种设计上的权衡,从而降低并发修改操作的检测代码对程序性能带来的影响。

当对象直接从容器中删除而不是通过 Iterator.remove 来删除时,即使是在单线程的程序中也会抛出 ConcurrentModificationException

要想避免出现 ConcurrentModificationException,就必须在迭代过程中持有容器的锁或者克隆容器,在副本上进行迭代。

5.1.3 隐藏迭代器

容器的 toStringhashCodeequals 等方法会间接的执行迭代操作,当容器作为另一个容器的元素或键值时,就会出现这种情况。

containsAllremoveAllretainAll 等方法,以及把容器作为参数的构造函数,都会对容器进行迭代。

所有这些简介的迭代操作都可能抛出 ConcurrentModificatcionException

5.2 并发容器

ConcurrentHashMap 用来替代同步基于散列的 Map

CopyOnWriteArrayList 用于在遍历操作为主要操作的情况下替代同步的 List

Queue 用来临时保存一组等待处理的元素,基于 LinkedList 实现。它提供了几种实现:ConcurrentLinkedQueue 是一个先进先出队列,PriorityQueue (非并发) 是一个优先队列。

Queue 上的操作不会阻塞,如果队列为空,那么获取元素的操作将返回空值。

BlockingQueue 扩展了 Queue,增加了可阻塞的插入和获取等操作。如果队列为空,那么获取元素的操作将会一直阻塞,直到队列中出现一个可用的元素。如果队列已满 (有界队列),那么插入元素的操作将一直阻塞,直到队列中出现可用的空间。

5.2.1 ConcurrentHashMap

ConcurrentHashMap 使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为分段锁。

Java 8 之前的实现机制

ConcurrentHashMap 返回的迭代器具有弱一致性,而并非及时失败。弱一致性的迭代器可以容忍并发的修改,当创建迭代器时会遍历已有的元素,并可以 (但是不保证) 在迭代器被构造后将修改操作反应给容器。

5.2.2 额外的原子 Map 操作

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);
}

5.2.3 CopyOnWriteArrayList

CopyOnWriteArrayList 用于替代同步 List,并且在迭代期间不需要对容器进行加锁或复制。

类似地,CopyOnWriteArraySet 的作用是替代同步 Set

"写入时复制 (Copy-On-Write)" 容器的线程安全性在于,只要正确地发布一个事实不可变的对象,那么在访问该对象时就不再需要进一步的同步。

在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。

"写入时复制" 容器的迭代器保留一个指向底层基础数组的引用,这个数组位于迭代器的起始位置,由于它不会被修改,因此在对其进行同步时只需确保数组内容的可见性。

"写入时复制" 容器返回的迭代器不会抛出 ConcurrentModificationException,并且返回的元素与迭代器创建时的元素完全一致,而不必考虑之后修改操作所带来的影响。

每当修改容器时都会复制底层数组,需要一定的开销,特别是当容器的规模较大时。

仅当迭代操作远远多于修改操作时,才应该使用 "写入时复制" 容器。

5.3 阻塞队列和生产者 - 消费者模式

阻塞队列提供了可阻塞的 puttake 方法,以及支持定时的 offerpoll 方法。

如果队列已经满了,那么 put 方法将阻塞直到有空间可用。

如果队列为空,那么 take 方法将会阻塞直到有元素可用。

BlockingQueue 有多种实现,其中,LinkedBlockingQueueArrayBlockingQueueFIFO 队列。

PriorityBlockingQueue 是一个按优先级排序的队列,可以按照某种顺序而不是 FIFO 来处理元素。既可以根据元素的自然顺序来比较元素 (如果它们实现了 Comparable 方法),也可以使用 Comparator 来比较。

SynchronousQueue 并不是一个真正的队列,因为它不会为队列中的元素维护存储空间。与其它队列不同的是,它维护一组线程,这些线程在等待着把元素加入或者移出队列。因为 SynchronousQueue 没有存储功能,因此 puttake 会一直阻塞,直到有另一个线程已经准备好参与到交付过程中。仅当有足够多的消费者,并且总是有一个消费者准备好获取交付的工作时,才适合使用该队列。

5.1.1 闭锁

闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。

闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。

当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。

闭锁可以用来确保某些活动直到其它活动都完成后才继续执行。

CountDownLatch 可以使一个或多个线程等待一组事件发生。