fail-fast 机制

大家好,我是指北君。

我的朋友小 B 今天去面试。面试结果有写惨淡。

面试官:能说说 fail-fast 是什么吗?

小 B:就是在多线程的时候,修改集合时引起 ConcurrentModificationException 异常的一种机制。

面试官:那么在单线程中,会出现吗?

小 B:不会出现。

面试官:你回去吧。

fail-fast 是什么

引用百度百科的数据:

fail-fast 机制是 java 集合 (Collection) 中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。例如:当某一个线程 A 通过 iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事件。

多线程?并发修改?才会引起 fail-fast 机制保护程序?小 B 觉得这个答案没有说全,面试官说了单线程也会引起 fail-fast 机制。那么百度百科对面试官谁对谁错写个 demo 就知道了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.ArrayList;
import java.util.List;

public class FastFailTest {

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();

        list.add("张三");
        list.add("李四");
        list.add("王五");
        list.add("赵六");

        for(String s : list) {
            if(s.equals("赵六")) {
                list.remove(s);
                System.out.println(list.toString());
            }
        }
    }
}

从下图的运行结果来看,list 已经完成了对赵六的 remove,说明并不是 remove 引发的问题,仔细查看异常原因:是在 ArrayList 的内部 Itr.checkForComodification() 方法出现的 ConcurrentModificationException 异常。小 B 感概了一句:网上资料不可尽信,动手实战才能出真知。

原理

将异常定位到报错的 ArrayList.java:911 行。

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

可以看到这个方法 checkForComodification 对 modCount 和 expectedModCount 进行了比较,如果不相同就抛出异常。modCount 和 expectedModCount 分别又时什么呢? remove 方法中不是修改了 modCount 就是修改了 expectedModCount。

modCount 被定义在 ArrayList 的父类 AbstractList 中,每一次调用 Add、Remove、Clear 等方法 modCount 就被 +1,可以说明这个变量的作用就是记录了 ArrayList 实际被修改的次数。

ArrayList 的 foreach 方法是用迭代器 Iterator 实现的,Iterator 在 ArrayList 中有一个实现类:Itr,它的成员变量 expectedModCount 在初始化的时候被赋值了 modCount。所以当 ArrayList 调用 remove 删除元素时,modCount 被 +1,此时不等于 expectedModCount,在 foreach 试图将局部变量 s 交接给下一个元素的时候,就出现了 ConcurrentModificationException 异常。

避免

经过分析,ConcurrentModificationException 时由于 modCount 和 expectedModCount 不一样导致的。

那么如何避免在循环的时候 add、remove 元素不抛出异常呢?

for 循环

使用普通的 for 循环,这样就可以不经过 Itr 内部类了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
List<String> list = new ArrayList<String>();

list.add("张三");
list.add("李四");
list.add("王五");
list.add("赵六");

for( int i = 0; i < list.size(); i++) {
    String s = list.get(i);
    if(s.equals("王五")) {
        list.remove(s);
        System.out.println(list.toString());
    }
}

示例结果是 [张三, 李四, 赵六] 没有出现异常。但是移除元素后面的索引已经被改变了。

迭代器 Iterator

直接使用迭代器 Iterator 中的方法,在它的remove 方法中显示的将 expectedModCount 赋值成 modCount。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Itr.remove()
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();
    }
}

定义一个迭代器局部变量,使用 hasNext() 方法控制 while 循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<String> list = new ArrayList<String>();

list.add("张三");
list.add("李四");
list.add("王五");
list.add("赵六");

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String s = iterator.next();
    if(s.equals("王五")) {
        iterator.remove();
        System.out.println(list.toString());
    }
}

CopyOnWriteArrayList

CopyOnWriteArrayList 是 java 并发包 java.util.concurrent 下面的类。它在操作 add、remove 元素时,先将原来的元素数组拷贝一份成为新的数组,在新数组上面做元素操作,修改完成后,将 CopyOnWriteArrayList 中数组的引用指向了新数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
List<String> list = new CopyOnWriteArrayList<String>();

list.add("张三");
list.add("李四");
list.add("王五");
list.add("赵六");

for(String s : list) {
    if(s.equals("李四")) {
        list.remove(s);
        System.out.println(list.toString());
    }
}

总结

fail-fast 机制就是不允许程序员不管是在单线程还是多线程环境中遍历集合的时候顺便还操作集合里面的元素。

我是指北君,操千曲而后晓声,观千剑而后识器。感谢各位人才的:点赞、收藏和评论,我们下期更精彩!

Java Geek Tech wechat
欢迎订阅 Java 技术指北,这里分享关于 Java 的一切。