侧边栏壁纸
博主头像
Lin2J博主等级

升级了服务器,访问应该会更加流畅🇨🇳

  • 累计撰写 94 篇文章
  • 累计创建 39 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

快速失败(fail-fast)和安全失败(fail-safe)机制

Lin2J
2021-03-22 / 0 评论 / 0 点赞 / 995 阅读 / 5,882 字 / 正在检测是否收录...

Java 的集合类中,在遍历修改时,会发生两种错误,一种是 fail-fast,一种是 fail-safe

这两种机制分别对应的是线程不安全和线程安全的集合类。

先说fail-fast ,在使用 Java集合的过程中,应该都碰到过 ConcurrentModificationException

我是大二那年进学校工作室考核期时,写程序的时候第一次碰到这个异常的。

原因就是我在for-each 中删除掉了一个元素。当时自然是懵懵的,也不知道这其实是 Java 集合的一个知识点:fail-fast

fail-fast 快速失败

如果在遍历一个集合的过程中,该集合对象的结构发生了改变,则集合会尽最大努力抛出 ConcurrentModificationException

存在两种情况,会抛出该异常:

单线程环境下

第一种是在单线程的环境下,使用 Iterator 对象遍历集合对象时,修改了集合对象的结构,比如增加或者删除元素。

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("a"); list.add("b"); list.add("c"); list.add("d");
        Iterator<String> itr = list.listIterator();
        while(itr.hasNext()){
            String s = itr.next();
            if (s.equals("b")) {
                list.remove(s);
            }
        }
        System.out.println(list);
    }

输出

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.jia.nabao.common.ListThreadSafeDemo.main(ListThreadSafeDemo.java:21)

使用 for-each 时,做修改也是一样的效果。因为 for-each 实际运行时也是通过 Iterator 对象进行工作的。

该异常是由 java.util.ArrayList.Itr#checkForComodification 方法抛出的,这个方法的源码如下:

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

通过判断modCountexpectedModCount 是否相等,可以判断出来集合结构是否被改变了。

modCount 是属于 ArrayList 的(实际上是在 AbstractList 声明的)。

expectedModCount 是属于 ListItr 的,ListItrArrayList 的迭代器,继承Itr并实现了ListIterator接口。

在初始化 ListItr 时,将 modCount 赋值给 expectedModCount,然后在迭代时,需要保证两者始终相等。

现在来看两个 remove 方法,然后就可以知道在这段程序中,modCountexpectedModCount 不相等的原因,以及什么时候会调用 checkForComodification 方法,为什么我前面说fail-fast 是尽最大努力抛出异常。

java.util.ArrayList#remove(int)

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

我们重点关注 modCount++ 这句代码,意思是结构发生改变时,在这里是删除了元素,modCount 自增以表示有一个操作改变了集合结构,添加或者删除。

能引起 modCount 改变的,大概还有这些方法(包括但不止这些):

ArrayList-modCount

再看迭代器的删除方式,java.util.ArrayList.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();
    }
}

可以看到 try 代码块中在移除元素之后,会将 modCount 赋值给 expectedModCount ,因此两个值可以保持相等。ArrayList.this.remove(lastRet); 调用的实际上就是上面讲的那个删除方法。

在这里,我们结合一下上面抛出异常的的示例代码,分析一下,modCountexpectedModCount 是什么时候不相等的,然后抛出异常需要什么条件。

我直接用截图加文字说明

modCount和expectedModCount

当我们观察输出结果时,会发现异常是从 next()方法抛出的,因为方法内部调用了 checkForComodification() 方法,从前面可以知道,该方法会判断两个数值是否相等,不相等时,会抛出异常。

至此,真相大白,是因为我们调用了 list.remove() 导致的,只要我们改为 itr.remove() 便可以避免该异常。

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 = i + 1;
    return (E) elementData[lastRet = i];
}

现在我们可以说一下,为什么是尽最大努力抛出异常了。

从前面 next() 方法我们可以知道一个点,就是如果不调用 next() 方法的话,就不会抛出异常。

所以没抛异常不代表你当前的操作是没问题,因为集合结构可能已经悄悄改变了,你当前的迭代器已经废了,凉凉。但是集合会在你还要继续用迭代器时,马上发出警告。

实际使用中,我认为不应该捕捉和依赖ConcurrentModificationException ,这个异常应该是用来检验代码是否有bug的。

多线程环境下

多线程环境下,如果多个线程对同一个集合进行修改,也是会出现 ConcurrentModificationException 异常的。

/**
 * 验证ArrayList的线程不安全,并提供几种线程安全的列表的解决方案
 *
 * @author linjinjia linjinjia047@163.com
 * @date 2021/3/19 22:38
 */
public class ListThreadSafeDemo {

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

输出(输出结果不是唯一的,也有可能不抛出异常):

ArrayListNotSafe

我们简单讲一下,为什么这里也抛出了异常。

通过输出结果,我们可以看到依旧是因为调用了 next() 方法才抛出这个异常。可是我们并没有在代码里使用过迭代器,为什么还会调用该方法呢?

这就要看到是谁调用了next() 方法了,通过调用方法栈,可以看到是调用了 java.util.AbstractCollection#toString 方法,可以看到这个方法是有用到迭代器的。

public String toString() {
    Iterator<E> it = iterator();
    if (! it.hasNext())
        return "[]";

    StringBuilder sb = new StringBuilder();
    sb.append('[');
    for (;;) {
        E e = it.next();
        sb.append(e == this ? "(this Collection)" : e);
        if (! it.hasNext())
            return sb.append(']').toString();
        sb.append(',').append(' ');
    }
}

说句题外话,为什么使用System.out.println(list); 会调用对象的 toString 呢?可以看看下面的源码。

public void println(Object x) {
    String s = String.valueOf(x);
    synchronized (this) {
        print(s);
        newLine();
    }
}

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

因此,在多线程环境下,并发修改集合的结构是可能抛出 ConcurrentModificationException 异常的。

java.util 里面的集合大部分都是 fail-fast 的,当然也有例外:比如 VectorHashTable

fail-safe 安全失败

起先我是不知道有这种机制的,我是只菜鸟。我是在网上了解 fail-fast 机制的时候,才知道有这个的。

fail-safe 机制是为线程安全的集合准备的,可以避免像fail-fast一样在并发使用集合的时候,不断地抛出异常。

正如前一句的描述,线程安全的集合的并发修改并不会抛出异常,fail-safe 并不能直观地看到它的表现。

我们可以通过 CopyOnWriteArrayList 类来看看,这个机制是怎么样的,当然看这个之前应该先对 CopyOnWriteArrayList 这个容器有个基本认识。可以看看这里

再看看 java.util.concurrent.CopyOnWriteArrayList.COWIterator ,它是 CopyOnWriteArrayList 的迭代器。COWIterator 在创建时,是直接使用 CopyOnWriteArrayList 的数组的,然后进行迭代的。

当容器的结构发生改变时,并不会影响迭代器数组。因为迭代器的数组只是 CopyOnWriteArrayList 的一个历史快照,而且也没有校验什么 modCount 的操作,所以不会抛出异常。

既然读取的是一个历史快照,那么在迭代器遍历的时候,如果集合的结构修改了,迭代器是不知道的。这就导致了说,遍历的内容并不是最新的,可能和集合的最新状态不一致,但是不会抛出异常,这就是安全失败的意思吧。

public static void main(String[] args) {
    List<String> list = new CopyOnWriteArrayList<>();
    list.add("a");
    list.add("b");
    list.add("c");
    list.add("d");
    Iterator<String> itr = list.listIterator();
    while (itr.hasNext()) {
        String s = itr.next();
        if (s.equals("b")) {
            list.remove(s);
        }
    }
    System.out.println(list);
}
0

评论区