当前位置: 代码迷 >> 综合 >> 快速失败(fail-fast)以及其中的问题与安全失败(fail-safe)
  详细解决方案

快速失败(fail-fast)以及其中的问题与安全失败(fail-safe)

热度:49   发布时间:2023-11-25 01:56:36.0

问题缘由

在写强化耗材功能时,需要判断一个逻辑:如果消耗了材料导致背包中的该材料数量为0了,需要在背包中移除该材料。

在移除操作时,直接遍历了该HashMap集合移除了集合中的指定元素,一旦判定到材料数为0时,就会抛出异常。

/* 错误写法:一旦判定到材料数为0时,就会抛出异常。 */
for(IItem iItem:iItemMap.values()){
    if(improveToxml:getItemId() == iItem.getItemId()){
    if(iItem.getAmount() >= improveToxml.getAmount()){
    iItem.setAmount(iItem.getAmount() - improveToxml.getAmount());if(iItem.getAmount() == 0){
    iItemMap.remove(iItem.getItemId());}}else{
    break;}}
}

经了解,发现原因是Java的快速失败(fail-fast)机制,于是更改了代码写法,使用Iterator对象的remove()方法。

/* 修改写法:使用Iterator对象的remove()方法。 */
Iterator<iItem> it = iItemMap.values().iterator();
while(it.hadNext()){
    IItem iItem = it.next;if(iItem.getAmount() >= improveToxml.getAmount()){
    iItem.setAmount(iItem.getAmount() - improveToxml.getAmount());if(iItem.getAmount() == 0){
    it.remove();}}else{
    break;}}
}

fail-fast与fail-safe

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

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

(1)fail-fast

先了解一下源头:Java的快速失败(fail-fast)机制。

  • 单线程
    当在用迭代器(Iterator)或者增强for循环(增强for循环的底层也是迭代器)对一个集合进行遍历操作时,如果遍历的过程中集合的结构发生了变化,就会抛出并发修改异常ConcurrentModificationException

  • 多线程
    当一个线程在对一个集合进行遍历操作的时候,如果其他线程对这个集合的结构进行了修改,就会抛出并发修改异常ConcurrentModificationException

注意:

单线程情况中,描述的集合的结构发生改变指的是增加或者删除元素,修改元素并不算结构的改变。

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

举例

  1. 在单线程的情况下,如果使用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();}
}
  1. 在多线程环境下,如果对集合对象进行并发修改,那么就会抛出ConcurrentModificationException异常。

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

原理:(以ArrayList为例子)

下面是ArrayList类的迭代器Iterator的内部实现类Itr源码:

实现的关键在于两个变量:modCountexpectedModCount

其中,modCount是ArrayList类中定义的,代表集合结构被修改的次数,也就是说无论是调用集合本身还是迭代器的元素增加或者删除操作,使集合结构发生了变化,modCount的值就会+1。

expectedModCount是ArrayList类的迭代器Iterator的内部实现类Itr中定义的变量,其初始值为modCount,如果集合结构变化是迭代器的remove()方法导致的,这个变量的值就会+1,如果是集合本身的remove()导致的,则值不会发生改变。

迭代器Itr的next()方法中首先会判断modCountexpectedModCount的值是否相等,如果不相等,则抛出ConcurrentModificationException异常。

注:由于增强for循环的底层就是迭代器实现,因此每遍历到一个元素,就会执行迭代器的next()方法。

private class Itr implements Iterator<E> {
    int cursor;       // 下一个要返回的元素的角标int lastRet = -1; // 最后一个元素的角标,如果集合长度为0,则lastRet = -1int expectedModCount = modCount;public boolean hasNext() {
    // 通过判断下一个元素的角标是否等于集合的大小来判断遍历过程中是否有下一个元素return cursor != size;}@SuppressWarnings("unchecked")public E next() {
    checkForComodification();int i = cursor;if (i >= size)throw new NoSuchElementException();Object[] elementData = ArrayList.this.elementData;if (i >= elementData.length)throw new ConcurrentModificationException();// 遍历到当前元素的时候,会将cursor+1,使其指向下一个元素cursor = i + 1;return (E) elementData[lastRet = i];}public void remove() {
    if (lastRet < 0)throw new IllegalStateException();checkForComodification();try {
    ArrayList.this.remove(lastRet);cursor = lastRet;lastRet = -1;// 迭代器的remove()方法执行结束之前会重新将modCount赋值给expectedModCount,// 以保证不会触发快速失败机制。expectedModCount = modCount;} catch (IndexOutOfBoundsException ex) {
    throw new ConcurrentModificationException();}}final void checkForComodification() {
    // 快速失败机制if (modCount != expectedModCount)throw new ConcurrentModificationException();}}

这里的关键在于cursor这个变量,它表示当前元素的下一个元素的角标,而且每次在next()方法执行结束之前会将这个值+1,这是正常情况。

那么,如果是使用迭代器对集合遍历过程中,使用了集合本身的remove()方法,将当前元素删除,那么后面的元素依次前移,cursor变量就会变成指向原集合中当前元素的下一个的下一个元素,此时如果继续遍历集合,再次执行迭代器的next()方法,就会发现modCount不等于expectedModCount,也就会抛出ConcurrentModificationException异常。

但是如果当前元素是集合中倒数第二个元素呢?此时原集合中当前元素的下一个的下一个元素不存在,而cursor变量的值会变成集合的size值,迭代器会以为集合已经遍历结束,从而中断遍历,也就不会再执行next()方法,更不会抛出ConcurrentModificationException异常。

这就是上面代码中为什么在增强for循环中使用了集合本身的remove()方法使集合结构发生了变化但却没有抛出ConcurrentModificationException异常,发生快速失败的原因。这样的话,输出结果中没有输出集合最后一个元素也是正常的,因为根本没有遍历到最后一个元素就提前结束了。

例:

List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
for (String s : list) {
    System.out.println(s);if (Objects.equals(s, "bbb")){
    list.remove(s);}
}
System.out.println(list);

输出结果:

aaa
bbb
[aaa, ccc]

在对集合的遍历过程中使用集合本身的remove()方法对集合元素进行了删除,按照Java的快速失败机制,应该会抛ConcurrentModificationException异常,但是事实上,这段代码执行没有任何问题。

再看输出结果可以看出,集合中的"bbb"元素已经被删除,但是删除之后,集合的最后一个元素"ccc"并没有被遍历到。

(2)fail-safe

Java在java.util.concurrent包中为我们提供了安全失败的集合类,如 ConcurrentHashMap、CopyOnWriteArrayList等。其原理在于开始遍历之前会将原集合完整的复制一份,前者在复制的集合上进行遍历,在原集合上进行元素添加、删除操作,后者反之,这样就可以避免了ConcurrentModificationException异常。

当然,这类集合也有一些缺点:iterator不能保证返回集合更新后的数据,因为其工作在集合克隆上,而非集合本身。其次,创建集合拷贝需要相应的开销,包括时间和内存。

例子:

// 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);}
}
  相关解决方案