Java集合中fail-fast和fail-safe机制详解

在使用集合时候,大家应该都遇到过或听过并发修改异常(ConcurrentModificationException),这其实是Java集合中的一种fail-fast机制,为了避免触发fail-fast机制,Java中还提供了一些采用fail-safe机制设计的集合类,本篇文章就系统的介绍一下这两种机制。

一、fail-fast机制

1.1 什么是fail-fast机制

简单的说,这就是系统设计的一种理念,就是在编码的时候优先考虑异常情况,一旦发生异常,就立即终止,并上报(抛异常),比如最经典的除0异常:

public int divide(int i,int j){
    if(j==0){
        throw new RuntimeException("被除数不能为零");
    }
    return i/j;
}

上述就是一个fail-fast的理念,快速失败,不做无用功

1.2 Java集合中的fail-fast机制

fail-fast机制是Java集合的一种并发检测机制,在进行foreach遍历时,都会提取保存modCount的值,然后每执行一次遍历add/remove操作前会进行比较,如果不一致就会抛出ConcurrentModificationException异常。

ps:除了JUC目录下的并发集合,Collection中所有Iterator的实现类(比如ArrayList、HashMap等)都是fail-fast的设计。

下面以ArrayList为例,来看下效果:

public static void main(String[] args) {
    List list=new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        list.add(i+"");
    }
    System.out.println("list = " + list);
    
    for (String str : list) {
        //list.add("hello"); 同样会抛异常
        list.remove(str);
    }
}

这是什么原因造成的呢?不要着急,在深入原理之前,我们先看下这个增强for的语法糖具体时如何实现的,我们使用jad工具,对编译后的class进行反编译,就会得到如下代码:

public static void main(String args[])
{
    List list = new ArrayList();
    for(int i = 0; i < 10; i++)
        list.add((new StringBuilder()).append(i).append("").toString());
​
    String str;
    for(Iterator iterator = list.iterator(); iterator.hasNext(); list.remove(str))
        str = (String)iterator.next();
​
}

不难发现,增强for是Iterator实现的,通过报错信息我们也可以定位到是在checkForComodification()方法中,该方法是在iterator.next()方法中调用的源码如下:

final void checkForComodification() {
   if (modCount != expectedModCount)
       throw new ConcurrentModificationException();
}

modCount我们并不陌生,无论是在HashMap还是ArrayList中都可以看到,它表示的是集合实际被修改的次数,而expectedModCount则是ArrayList中的一个内部类——Itr中的成员变量,部分源码如下:

private class Itr implements Iterator {
   int cursor;       // index of next element to return
   int lastRet = -1; // index of last element returned; -1 if no such
   int expectedModCount = modCount;
   //...
}

该类实现了Iterator接口,expectedModCount表示的是这个迭代器预期该集合被修改的次数,该值随着Itr被创建才会初始化,expectedModCount = modCount。

这下就真相大白了!上述测试代码在增强for遍历之前,modCount和expectedModCount的值都是10,第一轮遍历,可以正常remove,因为此时大家都是10。第一轮遍历之后,由于调用的是ArrayList的remove方法,modCount就变为了11,此时expectedModCount还是10,所以在进行第二轮遍历的时候,由于它俩不相等了,所以就发生了ConcurrentModificationException异常。

二、fail-safe机制

为了避免触发fail-fast机制,我们可以使用JUC包下的集合类,该包下的集合都是fail-safe的,在遍历的时候不是直接在原集合上访问的,而是先copy一份原集合的副本,然后在副本的基础上的再进行遍历,下面我们用CopyOnWriteArrayList举例:

public static void main(String[] args){
    List list=new CopyOnWriteArrayList<>();
    for (int i = 0; i < 10; i++) {
        list.add(i+"");
    }
    
    for (String str : list) {
        list.remove(str);
    }
    System.out.println("list = " + list);
}

Java集合中fail-fast和fail-safe机制详解_第1张图片

fail-safe集合的所有集合都是先拷贝一份副本,然后在副本上进行操作的,而且这些操作(add/remove)都是通过锁来控制并发的,所以在迭代的过程中不需要fail-fast的并发检测。

三、如何避免fail-fast机制

有同学可能有疑问,上面不是说了吗,用JUC下的集合类,当然这只是其中的一个避免方式,再说JUC下的都是线程安全的,而我们实际场景中还是非线程安全集合用的比较多,那么它们如何避免呢?

细心的同学可能会注意到我在fail-fast代码的例子中,用的是fori(普通集合遍历)来初始化list的,它并没有出现并发修改的异常,因为它本质上使用的ArrayList的remove方法,该方法不会进行并发检测。除此之外,还有几种方式也可以避免fail-fast机制。

3.1 普通for循环

public static void main(String[] args) {
    List list=new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        list.add(i+"");
    }
    System.out.println("list = " + list);
    for (int i = 0; i < list.size(); i++) {
       list.remove(i);
       i--;
    }
    System.out.println("list = " + list);
}

以下结果如果是这个,就不再赘述了:

list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
list = []

3.2 迭代器

public static void main(String[] args) {
    List list=new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        list.add(i+"");
    }
    System.out.println("list = " + list);
    Iterator itr = list.iterator();
    while (itr.hasNext()){
        itr.next();
        //这里用迭代器就没啥问题了
        itr.remove();
    }
    System.out.println("list = " + list);
}

我们可以看下迭代器remove的源码ArrayList中Itr内部类的方法

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();
    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        //保持同步
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

3.3 copy一份副本

将原来的copy出一个副本,注意要遍历原来的list,然后进行remove:

public static void main(String[] args) {
​
    List list=new ArrayList<>();
​
    for (int i = 0; i < 10; i++) {
        list.add(i+"");
    }
    System.out.println("list = " + list);
    List list1=new ArrayList<>(list);
    for (String s : list) {
        list1.remove(s);
    }
    System.out.println("list = " + list1);
}

3.4 Stream流

通过Stream的filter方法进行remove,因为Stream每次处理后都会生成一个新的Stream,不存在并发问题:

public static void main(String[] args) {
​
    List list=new ArrayList<>();
​
    for (int i = 0; i < 10; i++) {
        list.add(i);
    }
    System.out.println("list = " + list);
    List integers = list.stream().filter(i -> i < 5).collect(Collectors.toList());
    System.out.println("list = " + integers);
}
list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
list = [0, 1, 2, 3, 4]

3.5 removeIf方法

也是过滤删除,jdk 1.8开始,List提供了一个removeIf方法用于删除所有满足特定条件的元素:

public static void main(String[] args) {
    List list=new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        list.add(i);
    }
    System.out.println("list = " + list);
    list.removeIf(i->i>5);
    System.out.println("list = " + list);
}
list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
list = [0, 1, 2, 3, 4, 5]

 End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教。

你可能感兴趣的:(Java集合,java,集合的fail机制)