【JavaSE】fail-fast与fail-safe源码分析

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

文章目录

1. fail-fast与fail-safe概述

快速失败(fail-fast)快速失败是Java集合的一种错误检测机制。

  1. 出现场景线程A在使用迭代器遍历一个集合对象的时候线程B对集合对象的内存进行了操作增加、删除、修改这时候会抛出Concurrent Modification Exception
  2. 原理就拿ArrayList来说ArrayList继承了一个抽象类AbstractList这个抽象了中有一个成员变量protected transient int modCount = 0;这个变量是记录集合被修改的次数的。集合使用迭代器进行遍历的时候每当迭代器使用hashNext()/next()之前会先检测modCount变量是否为expectedmodCount值是的话就遍历否则抛出异常。
  3. 注意这里抛出异常的判断条件为modCount变量是否为expectedmodCount值如果集合发生变化时modCount值刚好又设置为了expectedmodCount值。因此不能依赖于这个异常是否抛出而进行并发操作的编程这个异常只建议用于检测并发修改的bug。
  4. 场景java.util包下的集合类都是快速失败的不能在多线程下发生并发修改迭代过程中被修改比如ArrayList 类。

安全失败fail—safe采用安全失败的集合容器在遍历时不是直接在集合内容上访问的而是先复制原有集合内容在拷贝的集合上进行遍历。

  1. 原理由于迭代时是对原集合的拷贝进行遍历所以在遍历过程中对原集合所作的修改并不能被迭代器检测到所以不会触发Concurrent Modification Exception
  2. 缺点基于拷贝内容的优点是避免了Concurrent Modification Exception但同样地迭代器并不能访问到修改后的内容也就是说迭代器遍历的是开始遍历那一刻拿到的集合拷贝在遍历期间原集合发生的修改迭代器是不知道的。
  3. 场景java.util.concurrent包下的容器都是安全失败可以在多线程下并发使用并发修改比如CopyOnWriteArrayList类。

2. fail-fast源码分析

测试代码使用IDEA进行debug

public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    list.add(4);
    for (Integer integer : list) {
        System.out.println(integer);
    }
}

这里的增强for循环实际上就是使用迭代器进行迭代的。迭代器记录了当前集合的修改次数。

int expectedModCount = modCount;

在for循环之前list集合中添加了四个元素所以在list中的modCount的值为4。

使用IDEA的debug工具在程序执行过程中往list集合中添加一个元素——5模拟并发。

image-20230201234022290

image-20230201234042067

成功往集合中添加第五个元素。

这时候当下一次增强for循环执行的时候也就是使用迭代器的hasNext方法

public boolean hasNext() {
    return cursor != size;
}

然后执行迭代器的next方法

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];
}

首先会调用checkForComodification()检验集合是否被修改如果修改那么集合中的mod不会与expectedModCount相等。

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

modexpectedModCount不相等则会抛出ConcurrentModificationException异常程序中断。

image-20230201234738430


3. fail-safe源码分析

使用CopyOnWriteArrayList进行测试。

public static void main(String[] args) {
    CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<Integer>();
    list.add(1);
    list.add(2);
    list.add(3);
    list.add(4);
    for (Integer integer : list) {
        System.out.println(integer);
    }
}

先来简单阅读一下CopyOnWriteArrayList的源码

//底层存放数据的数组
private transient volatile Object[] array;
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        //扩容
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

final Object[] getArray() {
    return array;
}

final void setArray(Object[] a) {
    array = a;
}

add方法它底层实现是加了ReentrantLock锁。

首先获取底层数组的长度。然后进行扩容。接着将需要添加进集合的元素放置在扩容后的数组的末端。

再将新数组newElements赋值给底层数组array

接下来在增强for循环打断点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qrm6VV5A-1675313411939)(https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/9944/image-20230202123227242.png)]

在准备开始增强for循环的时候会调用iterator创建一个COWIterator迭代器。

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

并且将底层存放数据的数组array作为参数。

//由后续调用next返回的元素的索引。
private int cursor;
//快照存储的是迭代器进行迭代的时候集合的数据
private final Object[] snapshot;
private COWIterator(Object[] elements, int initialCursor) {
    cursor = initialCursor;
    snapshot = elements;
}

然后调用hasNext方法

public boolean hasNext() {
    return cursor < snapshot.length;
}

接着调用next方法将快照的数组依次输出。

public E next() {
    if (! hasNext())
        throw new NoSuchElementException();
    return (E) snapshot[cursor++];
}

假如这时候线程B向集合中添加一个元素。但是迭代器中的snapshot并没有变化。

因为能snapshotCOWIterator初始化的时候已经固定了。即使接下来array发生改变。snapshot也依然不变。

所以迭代器输出的数据是迭代器创建那一刻之前的数据迭代过程中无论集合原数据变成什么都是输出旧数据。


4. 总结

  1. ArrayListfail-fast的经典代表遍历的同时不能修改否则会抛出异常。
  2. CopyOnWriteArrayListfail-safe的经典代表遍历的同时可以修改原理是读写分离。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Java