大白话之 fail-fast | fail-safe:为什么会有这个机制?它有什么作用?

本贴最后更新于 1929 天前,其中的信息可能已经事过景迁

前言

阅读本篇文章,你需要了解下列知识:

  • 多线程的实现(看过来
  • Iterator 的使用
  • ArrayList 的使用和如何实现 Iterator

为什么会有这个机制?

举个栗子

  1. 有一杯水、两个人(黄渤和红雷)
  2. 黄渤拿起了水杯,开始喝水
  3. 红雷到达案发现场,想走水杯喝水
  4. 黄渤很生气,并锤了红雷一顿

映射关系

将上面的栗子翻译一下:

  1. 有一个 ArrayList、两个线程(Thread1Thread2
  2. Thread1 请求并开始使用 Iterator 遍历 ArrayList
  3. Thread2 随后紧跟请求对 ArrayList 进行修改
  4. 由于 Thread1 正在遍历 ArrayListArrayListThread2 扔出 ConcurrentModificationException

继承关系

Tip 如果下面的知识让你难以搞懂,可以直接跳过,在深入学习接口和继承后,再回来看一遍。

6.png

看起来有些困难?没关系。

ArrayList继承了Iterable接口,实现了基于Iterator的遍历功能。

ArrayList 实现 Iterator 的部分源码如下:

    /**
     * An optimized version of AbstractList.Itr
     */
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        // prevent creating a synthetic constructor
        Itr() {}

        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 = 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;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        public void forEachRemaining(Consumer<? super E> action) {
            Objects.requireNonNull(action);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i < size) {
                final Object[] es = elementData;
                if (i >= es.length)
                    throw new ConcurrentModificationException();
                for (; i < size && modCount == expectedModCount; i++)
                    action.accept(elementAt(es, i));
                // update once at end to reduce heap write traffic
                cursor = i;
                lastRet = i - 1;
                checkForComodification();
            }
        }

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

throw new ConcurrentModificationException();Itr 类中被赋予了四次条件执行:

方法 字段 触发条件
next() if (i >= elementData.length) throw new ConcurrentModificationException(); 当枚举的指针大于等于 ArrayList 的长度时,抛出错误
remove() try {ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException(); } 当删除 ArrayList 中指定对象时出错(对象不存在),抛出错误
forEachRemaining() if (i >= es.length) throw new ConcurrentModificationException(); 当枚举的指针大于等于 ArrayList 的长度时,抛出错误
checkForComodification() if (modCount != expectedModCount) throw new ConcurrentModificationException(); 检测当运行中统计对象的数量不等于预计中对象的数量时,抛出错误

它有什么作用?

当多个线程同时对一个对象进行高频率的增删改查时,可能会出现数据异常。

实例

例子很简单,我们创建两个线程,Thread1 访问修改 ArrayListThread2Thread1 尚未访问完毕时同时对 ArrayList 进行访问修改:

打开你的 IDE,新建类 Main.java 并复制下方代码:

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

public class Main {
    //需要操作的ArrayList
    private static List arrayList = new ArrayList<>();

    public static void main(String[] args) {
        //将类Thr实例化为两个线程
        Thread thread1 = new Thr();
        Thread thread2 = new Thr();
        //同时启动两个线程进行操作
        thread1.start();
        thread2.start();
    }

    private static void printAll(String threadName) {
        System.out.println("由线程" + threadName + "遍历后,当前ArrayList的内容:");
        //获取ArrayList的Iterator枚举器
        Iterator iterator = arrayList.iterator();
        //将ArrayList中全部内容遍历并打印
        while(iterator.hasNext()) {
            System.out.print(iterator.next());
        }
        System.out.println();
    }

    static class Thr extends Thread {
        public void run() {
            System.out.println("线程" + Thread.currentThread().getName() + "开始运行!当前时间" + new SimpleDateFormat("mm:ss:SS").format(new Date()));
            //将0-5写入到ArrayList中,每次写入打印一次ArrayList中全部的内容
            for (int i = 0; i < 5; i++) {
                //将i写入到ArrayList
                arrayList.add(i);
                //打印ArrayList中全部的内容
                printAll(Thread.currentThread().getName());
            }
        }
    }

}

运行结果:

线程Thread-0开始运行!当前时间14:04:358
线程Thread-1开始运行!当前时间14:04:359
由线程Thread-1遍历后,当前ArrayList的内容:
0由线程Thread-0遍历后,当前ArrayList的内容:
00
由线程Thread-0遍历后,当前ArrayList的内容:
001
由线程Thread-0遍历后,当前ArrayList的内容:
0012
由线程Thread-0遍历后,当前ArrayList的内容:
0Exception in thread "Thread-1" 00123
由线程Thread-0遍历后,当前ArrayList的内容:
0java.util.ConcurrentModificationException
01234
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1042)
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:996)
	at failfast.Main.printAll(Main.java:28)
	at failfast.Main$Thr.run(Main.java:41)

请注意 你的运行结果可能和我的运行结果会有以下两种偏差:

  1. 两个线程的启动顺序、启动时间
  2. 可能不会产生报错(由于两个线程读取 ArrayList 的时间恰巧错开)

你可能发现了一个规律:

结果是随机的,最重要的是 ConcurrentModificationException 是不稳定的。即使两个线程同时访问,也有一部分可能性不会抛出 ConcurrentModificationException。

为什么呢?让我们看看官方对 fail-fast机制 的解释:

下方解释部分来源于 https://www.cnblogs.com/kubidemanong/articles/9113820.html

迭代器的 fail-fast 行为是不一定能够得到保证的。一般来说,存在非同步的并发修改时,是不能够保证错误一定被抛出的。但是会做出最大的努力来抛出 ConcurrentModificationException

因此,编写依赖于此异常的程序的做法是不正确的。正确的做法应该是:迭代器的 fail-fast 行为应该仅用于检测程序中的 Bug。

避免 fail-fast

ArrayList 使用 fail-fast机制 自然是因为它增强了数据的安全性。但在某些场景,我们可能想避免 fail-fast机制 产生的错误,这时我们就要将 ArrayList 替换为使用 fail-safe 机制的 CopyOnWriteArrayList

将:

private static List arrayList = new ArrayList<>();

修改为:

private static List arrayList = new CopyOnWriteArrayList<>();

CopyOnWriteArrayListIterator 的实现上没有设计抛出 ConcurrentModificationException 的代码段,所以便避免了 fail-fast机制 错误的抛出。我们将它称之为 fail-safe机制

后语

本篇篇幅较长,涉及的知识点较乱。如果你有问题,可以在下方评论中提出;如果你发现本篇文章中存在误导内容,也请在评论中及时告知,谢谢!

  • 大白话
    17 引用 • 27 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3190 引用 • 8214 回帖 • 1 关注
  • 原理
    16 引用 • 44 回帖
  • 运行机制
    1 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...