大白话之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

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

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机制

后语

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

如转载请在文章尾部添加

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

评论

取消