在 Collection 集合的各个类中,有线程安全和线程不安全两大类的版本。

  • 线程不安全的类
    • 并发情况下可能会出现 fail-fast 情况;
  • 线程安全的类
    • 可能出现 fail-safe 的情况。

      1. 并发修改

      当一个或多个线程正在遍历一个集合 Collection 的时候(Iterator遍历),而此时另一个线程修改了这个集合的内容(如添加,删除或者修改)。这就是并发修改的情况。

2. fail-fast 快速失败

**fail-fast 机制**:当遍历一个集合对象时,如果集合对象的结构被修改了,就会抛出ConcurrentModificationExcetion 异常。

有 2 种情况会抛出该异常:

  1. 在单线程的情况下,如果使用 Iterator 对象遍历集合对象的过程中,修改了集合对象的结构。如下:
    1. // 1.iterator迭代,抛出ConcurrentModificationException异常
    2. Iterator<String> iterator = list.iterator();
    3. while (iterator.hasNext()) {
    4. String s = iterator.next();
    5. System.out.println(s);
    6. // 修改集合结构
    7. if ("s2".equals(s)) {
    8. list.remove(s);
    9. }
    10. }
    11. // 2.foreach迭代,抛出ConcurrentModificationException异常
    12. for (String s : list) {
    13. System.out.println(s);
    14. // 修改集合结构
    15. if ("s2".equals(s)) {
    16. list.remove(s);
    17. }
    18. }

要想避免抛出异常,应该使用 Iterator 对象的 remove() 方法。

  1. // 3.iterator迭代,使用iterator.remove()移除元素不会抛出异常
  2. Iterator<String> iterator2 = list.iterator();
  3. while (iterator2.hasNext()) {
  4. String s = iterator2.next();
  5. System.out.println(s);
  6. // 修改集合结构
  7. if ("s2".equals(s)) {
  8. iterator2.remove();
  9. }
  10. }
  1. 在多线程环境下,如果对集合对象进行并发修改,那么就会抛出 ConcurrentModificationException 异常。

**注意**: 迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法,迭代器的快速失败行为应该仅用于检测 bug。

2.1 以 ArrayList 为例,讲解一下 fail-fast 的机制

2.1.1 单线程下,使用 iterator 迭代时的情况

ArrayList 继承自 AbstractList 类,AbstractList 内部有一个字段 modCount代表修改的次数
快速失败(fail-fast)与安全失败(fail-safe) - 图1
ArrayList 类的 add、remove 操作都会使得 **modCount** 自增。

快速失败(fail-fast)与安全失败(fail-safe) - 图2
快速失败(fail-fast)与安全失败(fail-safe) - 图3
当使用 ArrayList.iterator() 返回一个迭代器对象时。迭代器对象有一个属性 **expectedModCount**,它被赋值为该方法调用时 modCount 的值。这意味着,这个值是 **modCount** 在这个时间点的快照值,而**expectedModCount** 值在 **iterator** 对象内部不会再发送变化!

快速失败(fail-fast)与安全失败(fail-safe) - 图4

这时候我们就能明白了,在得到迭代器之后,如果我们使用 ArrayList 的 add、remove 等方法,会使得 modCount 的 值自增(发生了变化),而 iterator 内部的 expectedModCount 值却还是之前的快照值。

我们再来看 iterator 的方法实现:可以看到,在调用next方法时,第一步就是检查**modCount**值和迭代器内部的**expectedModCount**值是否相等!显然,这是不等的,所以在调用next方法的时候,就抛出了 ConcurrentModificationException异常。
快速失败(fail-fast)与安全失败(fail-safe) - 图5
快速失败(fail-fast)与安全失败(fail-safe) - 图6

为什么说迭代器的 fail-fast 机制是尽最大努力地抛出**ConcurrentModificationException**异常呢?

原因就是上面我们看到的,只有在迭代过程中修改了元素的结构,当再调用 next() 方法时才会抛出该异常。也就是说,如果迭代过程中发生了修改,但之后没有调用 next() 迭代,该异常就不会抛出了!(该异常的机制是告诉你,当前迭代器要进行操作是有问题的,因为集合对象现在的状态发生了改变!)

那为什么 **iterator.remove()** 方法可行呢?

下图中,可以看到,remove 方法没有进行 modCount 值的检查,并且手动把 expectedModCount 值修改成了modCount 值,这又保证了下一次迭代的正确。
快速失败(fail-fast)与安全失败(fail-safe) - 图7

2.1.2 多线程下的情况

当然,如果多线程下使用迭代器也会抛出 ConcurrentModificationException 异常。而如果不进行迭代遍历,而是并发修改集合类,则可能会出现其他的异常如数组越界异常。

3. fail-safe 安全失败

Fail-Safe 迭代的出现,是为了解决 fail-fast 抛出异常处理不方便的情况。fail-safe 是针对线程安全的集合类

上面的 fail-fast 发生时,程序会抛出异常,而 fail-safe 是一个概念,并发容器的并发修改不会抛出异常,这和其实现有关。并发容器的 iterate 方法返回的 iterator 对象,内部都是保存了该集合对象的一个快照副本,并且没有modCount 等数值做检查。如下图,这也造成了并发容器的 iterator 读取的数据是某个时间点的快照版本。你可以并发读取,不会抛出异常,但是不保证你遍历读取的值和当前集合对象的状态是一致的!这就是**安全失败的含义**

快速失败(fail-fast)与安全失败(fail-safe) - 图8

所以 Fail-Safe 迭代的缺点是

  • 首先是 iterator 不能保证返回集合更新后的数据,因为其工作在集合克隆上,而非集合本身。
  • 其次,创建集合拷贝需要相应的开销,包括时间和内存。

java.util.concurrent 包中集合的迭代器,如 ConcurrentHashMap, CopyOnWriteArrayList 等默认为都是 Fail-Safe。

  1. // 1.foreach迭代,fail-safe,不会抛出异常
  2. for (String s : list) {
  3. System.out.println(s);
  4. if ("s1".equals(s)) {
  5. list.remove(s);
  6. }
  7. }
  8. // 2.iterator迭代,fail-safe,不会抛出异常
  9. Iterator<String> iterator = list.iterator();
  10. while (iterator.hasNext()) {
  11. String s = iterator.next();
  12. System.out.println(s);
  13. if ("s1".equals(s)) {
  14. list.remove(s);
  15. }
  16. }