fail-fast与fail-safe
在Collection集合的各个类中,有线程安全和线程不安全这2大类的版本。
对于线程不安全的类,并发情况下可能会出现fail-fast情况;而线程安全的类,可能出现fail-safe的情况。
一、并发修改
当一个或多个线程正在遍历一个集合Collection的时候(Iterator遍历),而此时另一个线程修改了这个集合的内容(如添加,删除或者修改)。这就是并发修改的情况。
二、fail-fast快速失败
fail-fast机制:当遍历一个集合对象时,如果集合对象的结构被修改了,就会抛出ConcurrentModificationExcetion异常。
有2种情况会抛出该异常:
-
在单线程的情况下,如果使用Iterator对象遍历集合对象的过程中,修改了集合对象的结构。如下:
// 1.iterator迭代,抛出ConcurrentModificationException异常 Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String s = iterator.next(); System.out.println(s); // 修改集合结构 if ("s2".equals(s)) { list.remove(s); } } // 2.foreach迭代,抛出ConcurrentModificationException异常 for (String s : list) { System.out.println(s); // 修改集合结构 if ("s2".equals(s)) { list.remove(s); } }
要想避免抛出异常,应该使用Iterator对象的remove()方法。
// 3.iterator迭代,使用iterator.remove()移除元素不会抛出异常 Iterator<String> iterator2 = list.iterator(); while (iterator2.hasNext()) { String s = iterator2.next(); System.out.println(s); // 修改集合结构 if ("s2".equals(s)) { iterator2.remove(); } }
- 在多线程环境下,如果对集合对象进行并发修改,那么就会抛出ConcurrentModificationException异常。
注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法,迭代器的快速失败行为应该仅用于检测 bug。
以ArrayList为例,讲解一下fail-fast的机制
1、单线程下,使用iterator迭代时的情况
ArrayList继承自AbstractList类,AbstractList内部有一个字段modCount,代表修改的次数。
ArrayList类的add、remove操作都会使得modCount自增。
当使用ArrayList.iterator()返回一个迭代器对象时。迭代器对象有一个属性expectedModCount,它被赋值为该方法调用时modCount的值。这意味着,这个值是modCount在这个时间点的快照值,expectedModCount值在iterator对象内部不会再发送变化!
这时候我们就能明白了,在得到迭代器之后,如果我们使用ArrayList的add、remove等方法,会使得modCount的值自增(发生了变化),而iterator内部的expectedModCount值却还是之前的快照值。我们再来看iterator的方法实现:可以看到,在调用next方法时,第一步就是检查modCount值和迭代器内部的expectedModCount值是否相等!显然,这是不等的,所以在调用next方法的时候,就抛出了ConcurrentModificationException异常。
为什么说迭代器的fail-fast机制是尽最大努力地抛出ConcurrentModificationException异常呢?
原因就是上面我们看到的,只有在迭代过程中修改了元素的结构,当再调用next()方法时才会抛出该异常。也就是说,如果迭代过程中发生了修改,但之后没有调用next()迭代,该异常就不会抛出了!(该异常的机制是告诉你,当前迭代器要进行操作是有问题的,因为集合对象现在的状态发生了改变!)
那为什么iterator.remove()方法可行呢?
下图中,可以看到,remove方法没有进行modCount值的检查,并且手动把expectedModCount值修改成了modCount值,这又保证了下一次迭代的正确。
2、多线程下的情况
当然,如果多线程下使用迭代器也会抛出ConcurrentModificationException异常。而如果不进行迭代遍历,而是并发修改集合类,则可能会出现其他的异常如数组越界异常。
三、fail-safe安全失败
Fail-Safe 迭代的出现,是为了解决fail-fast抛出异常处理不方便的情况。fail-safe是针对线程安全的集合类。
上面的fail-fast发生时,程序会抛出异常,而fail-safe是一个概念,并发容器的并发修改不会抛出异常,这和其实现有关。并发容器的iterate方法返回的iterator对象,内部都是保存了该集合对象的一个快照副本,并且没有modCount等数值做检查。如下图,这也造成了并发容器的iterator读取的数据是某个时间点的快照版本。你可以并发读取,不会抛出异常,但是不保证你遍历读取的值和当前集合对象的状态是一致的!这就是安全失败的含义。
所以Fail-Safe 迭代的缺点是:首先是iterator不能保证返回集合更新后的数据,因为其工作在集合克隆上,而非集合本身。其次,创建集合拷贝需要相应的开销,包括时间和内存。
在java.util.concurrent 包中集合的迭代器,如 ConcurrentHashMap, CopyOnWriteArrayList等默认为都是Fail-Safe。
// 1.foreach迭代,fail-safe,不会抛出异常
for (String s : list) {
System.out.println(s);
if ("s1".equals(s)) {
list.remove(s);
}
}
// 2.iterator迭代,fail-safe,不会抛出异常
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
System.out.println(s);
if ("s1".equals(s)) {
list.remove(s);
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。