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

前言

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

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

为什么会有这个机制?

举个栗子

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

映射关系

将上面的栗子翻译一下:

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

继承关系

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

6.png

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

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

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

 1    /**
 2     * An optimized version of AbstractList.Itr
 3     */
 4    private class Itr implements Iterator<E> {
 5        int cursor;       // index of next element to return
 6        int lastRet = -1; // index of last element returned; -1 if no such
 7        int expectedModCount = modCount;
 8
 9        // prevent creating a synthetic constructor
10        Itr() {}
11
12        public boolean hasNext() {
13            return cursor != size;
14        }
15
16        @SuppressWarnings("unchecked")
17        public E next() {
18            checkForComodification();
19            int i = cursor;
20            if (i >= size)
21                throw new NoSuchElementException();
22            Object[] elementData = ArrayList.this.elementData;
23            if (i >= elementData.length)
24                throw new ConcurrentModificationException();
25            cursor = i + 1;
26            return (E) elementData[lastRet = i];
27        }
28
29        public void remove() {
30            if (lastRet < 0)
31                throw new IllegalStateException();
32            checkForComodification();
33
34            try {
35                ArrayList.this.remove(lastRet);
36                cursor = lastRet;
37                lastRet = -1;
38                expectedModCount = modCount;
39            } catch (IndexOutOfBoundsException ex) {
40                throw new ConcurrentModificationException();
41            }
42        }
43
44        @Override
45        public void forEachRemaining(Consumer<? super E> action) {
46            Objects.requireNonNull(action);
47            final int size = ArrayList.this.size;
48            int i = cursor;
49            if (i < size) {
50                final Object[] es = elementData;
51                if (i >= es.length)
52                    throw new ConcurrentModificationException();
53                for (; i < size && modCount == expectedModCount; i++)
54                    action.accept(elementAt(es, i));
55                // update once at end to reduce heap write traffic
56                cursor = i;
57                lastRet = i - 1;
58                checkForComodification();
59            }
60        }
61
62        final void checkForComodification() {
63            if (modCount != expectedModCount)
64                throw new ConcurrentModificationException();
65        }
66    }

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并复制下方代码:

 1import java.text.SimpleDateFormat;
 2import java.util.ArrayList;
 3import java.util.Date;
 4import java.util.Iterator;
 5import java.util.List;
 6
 7public class Main {
 8    //需要操作的ArrayList
 9    private static List arrayList = new ArrayList<>();
10
11    public static void main(String[] args) {
12        //将类Thr实例化为两个线程
13        Thread thread1 = new Thr();
14        Thread thread2 = new Thr();
15        //同时启动两个线程进行操作
16        thread1.start();
17        thread2.start();
18    }
19
20    private static void printAll(String threadName) {
21        System.out.println("由线程" + threadName + "遍历后,当前ArrayList的内容:");
22        //获取ArrayList的Iterator枚举器
23        Iterator iterator = arrayList.iterator();
24        //将ArrayList中全部内容遍历并打印
25        while(iterator.hasNext()) {
26            System.out.print(iterator.next());
27        }
28        System.out.println();
29    }
30
31    static class Thr extends Thread {
32        public void run() {
33            System.out.println("线程" + Thread.currentThread().getName() + "开始运行!当前时间" + new SimpleDateFormat("mm🇸🇸SS").format(new Date()));
34            //将0-5写入到ArrayList中,每次写入打印一次ArrayList中全部的内容
35            for (int i = 0; i < 5; i++) {
36                //将i写入到ArrayList
37                arrayList.add(i);
38                //打印ArrayList中全部的内容
39                printAll(Thread.currentThread().getName());
40            }
41        }
42    }
43
44}

运行结果:

 1线程Thread-0开始运行!当前时间14:04:358
 2线程Thread-1开始运行!当前时间14:04:359
 3由线程Thread-1遍历后,当前ArrayList的内容:
 40由线程Thread-0遍历后,当前ArrayList的内容:
 500
 6由线程Thread-0遍历后,当前ArrayList的内容:
 7001
 8由线程Thread-0遍历后,当前ArrayList的内容:
 90012
10由线程Thread-0遍历后,当前ArrayList的内容:
110Exception in thread "Thread-1" 00123
12由线程Thread-0遍历后,当前ArrayList的内容:
130java.util.ConcurrentModificationException
1401234
15	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1042)
16	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:996)
17	at failfast.Main.printAll(Main.java:28)
18	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

将:

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

修改为:

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

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

后语

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

如转载请在文章尾部添加

原作者来自 adlered 个人技术博客:https://www.stackoverflow.wiki/

评论

取消