说说快速失败和安全失败是什么

1. 快速失败(fail-fast)

1.1 现象

在我们常见的java集合中就可能出现fail-fast机制,比如ArrayList,HashMap。在多线程和单线程环境下都有可能出现快速失败。
单线程:
定义了一个Arraylist集合,并使用迭代器遍历,在遍历过程中,刻意在某一步迭代中remove一个元素,这个时候,就会发生fail-fast。
多线程:
假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

1.2 fail-fast产生原因

现在我们来看看ArrayList中迭代器的源代码:

private class Itr implements Iterator<E> {
  int cursor;
    int lastRet = -1;
    int expectedModCount = ArrayList.this.modCount;

    public boolean hasNext() {
        return (this.cursor != ArrayList.this.size);
    }

    public E next() {
        checkForComodification();
        /** 省略此处代码 */
    }

    public void remove() {
        if (this.lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();
        /** 省略此处代码 */
    }

    final void checkForComodification() {
        if (ArrayList.this.modCount == this.expectedModCount)
            return;
        throw new ConcurrentModificationException();
    }
}

从上面的源代码我们可以看出,迭代器在调用next()、remove()方法时都是调用checkForComodification()方法,该方法主要就是检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。所以要弄清楚为什么会产生fail-fast机制我们就必须要用弄明白为什么modCount != expectedModCount ,他们的值在什么时候发生改变的。

expectedModCount 是在Itr中定义的:int expectedModCount = ArrayList.this.modCount;所以他的值是不可能会修改的,所以会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量:

protected transient int modCount = 0;

modCount什么时候因为什么原因而发生改变呢?请看ArrayList的源码:

public boolean add(E paramE) {
     ensureCapacityInternal(this.size + 1);
     /** 省略此处代码 */
 }

 private void ensureCapacityInternal(int paramInt) {
     if (this.elementData == EMPTY_ELEMENTDATA)
         paramInt = Math.max(10, paramInt);
     ensureExplicitCapacity(paramInt);
 }
 
 private void ensureExplicitCapacity(int paramInt) {
     this.modCount += 1;    //修改modCount
     /** 省略此处代码 */
 }
 
public boolean remove(Object paramObject) {
     int i;
     if (paramObject == null)
         for (i = 0; i < this.size; ++i) {
             if (this.elementData[i] != null)
                 continue;
             fastRemove(i);
             return true;
         }
     else
         for (i = 0; i < this.size; ++i) {
             if (!(paramObject.equals(this.elementData[i])))
                 continue;
             fastRemove(i);
             return true;
         }
     return false;
 }

 private void fastRemove(int paramInt) {
     this.modCount += 1;   //修改modCount
     /** 省略此处代码 */
 }

 public void clear() {
     this.modCount += 1;    //修改modCount
     /** 省略此处代码 */
 }

从上面的源代码我们可以看出,ArrayList中无论add、remove、clear方法只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。所以我们这里可以初步判断由于expectedModCount 得值与modCount的改变不同步,导致两者之间不等从而产生fail-fast机制。知道产生fail-fast产生的根本原因了,我们可以有如下场景:

有两个线程(线程A,线程B),其中线程A负责遍历list、线程B修改list。线程A在遍历list过程的某个时候(此时expectedModCount = modCount=N),线程启动,同时线程B增加一个元素,这是modCount的值发生改变(modCount + 1 = N + 1)。线程A继续遍历执行next方法时,通告checkForComodification方法发现expectedModCount = N ,而modCount = N + 1,两者不等,这时就抛出ConcurrentModificationException 异常,从而产生fail-fast机制。

所以,直到这里我们已经完全了解了fail-fast产生的根本原因了。知道了原因就好找解决办法了。

1.3 fail-fast解决办法

方案1:在单线程的遍历过程中,调用迭代器的remove方法而不是集合类的remove方法

在单线程的遍历过程中,如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法。

迭代器的remove方法并不会修改modCount的值,并且不会对后面的遍历造成影响,因为该方法remove不能指定元素,只能remove当前遍历过的那个元素,所以调用该方法并不会发生fail-fast现象。该方法有局限性。

public static void main(String[] args) {
      List<String> list = new ArrayList<>();
      for (int i = 0 ; i < 10 ; i++ ) {
           list.add(i + "");
      }
      Iterator<String> iterator = list.iterator();
      int i = 0 ;
      while(iterator.hasNext()) {
           if (i == 3) {
                iterator.remove(); //迭代器的remove()方法
           }
           System.out.println(iterator.next());
           i ++;
      }
}

方案2:所有涉及到改变modCount值得地方全部加上synchronized

在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。

方案3:使用java并发包(java.util.concurrent)中的类来代替 ArrayList 和hashMap

比如使用 CopyOnWriterArrayList代替 ArrayList, CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。该容器在对array在结构上有所改变的操作时(add、remove、clear等),并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组,所以对于 CopyOnWriterArrayList在迭代过程并不会发生fail-fast现象。但 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。

对于HashMap,可以使用ConcurrentHashMap, ConcurrentHashMap采用了锁机制,是线程安全的。在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。即迭代不会发生fail-fast,但不保证获取的是最新的数据。

2. 安全失败(fail-safe)

2.1 原理

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常

2.2 缺点

迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

2.3 场景

java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

2.4 使用

CopyOnWriterArrayList原理

ConcurrentHashMap使用

ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap();
concurrentHashMap.put("不只Java-1", 1);
concurrentHashMap.put("不只Java-2", 2);
concurrentHashMap.put("不只Java-3", 3);

Set set = concurrentHashMap.entrySet();
Iterator iterator = set.iterator();

while (iterator.hasNext()) {
    System.out.println(iterator.next());
    concurrentHashMap.put("下次循环正常执行", 4);
}
System.out.println("程序结束");

在这里插入图片描述

总结

快速失败和安全失败是对迭代器而言的。
快速失败:当在迭代一个集合的时候,如果有另外一个线程在修改这个集合,就会抛出ConcurrentModification异常,java.util下都是快速失败。
安全失败:在迭代时候会在集合二层做一个拷贝,所以在修改集合上层元素不会影响下层。在java.util.concurrent下都是安全失败

来源:https://blog.csdn.net/qq_31780525/article/details/77431970
https://codeleading.com/article/69863321152/
快速失败机制:https://chenssy.blog.csdn.net/article/details/38151189
快速失败机制:https://blog.csdn.net/zymx14/article/details/78394464

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值