浅谈fail-fast和fail-safe

1.引入

遍历一个集合,我们有三种办法:

  1. for循环
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
  1. foreach
        for (int i : list) {
            System.out.println(i);
        }
  1. 迭代器Iterator
        while (iterator.hasNext()){
            int next = (int) iterator.next();
            System.out.println(next);
        }

单线程情况下这三种遍历都能正常运行,而迭代器的方式在数据量大的情况下是很快的。
看一下Iterator的源码

public interface Iterator<E> {
	//如果迭代有更多元素,则返回true 
    boolean hasNext();

    //返回迭代中的下一个元素
    E next();

    //从底层集合中移除此迭代器返回的最后一个元素(可选操作)。
    
     每次调用next只能调用此方法一次。 如果在迭代正在进行时以除调用此方法
     
     以外的任何方式修改了基础集合,则迭代器的行为是未指定的
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    //对每个剩余元素执行给定的操作,直到处理完所有元素或操作引发异常。 
    
    如果指定了该顺序,则操作按迭代顺序执行。 
    
    动作抛出的异常被转发给调用者。
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

我们通常使用的只有hasNext()方法和next()方法,如果我们在迭代时调用remove会怎样呢?

    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            int next = (int) iterator.next();
            iterator.remove();
        }
        System.out.println(list.size());;
    }

在这里插入图片描述
可以看到ArrayList里面的元素被删除了,源码中提出“以此方式以外的修改集合元素则迭代器的行为不确定",那我们来试一下用另一个线程迭代时改动。

    private static List<Integer> list = new ArrayList<>();

    private static class thread1 extends Thread {
        @SneakyThrows
        @Override
        public void run() {
            Iterator<Integer> iterator = list.iterator();
            while (iterator.hasNext()) {
                int i = iterator.next();
                System.out.println("thread1遍历:" + i);
                Thread.sleep(100);
            }
        }
    }

    private static class thread2 extends Thread {
        @SneakyThrows
        @Override
        public void run() {
            Thread.sleep(200);
            list.remove(9);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }
        new thread1().start();
        new thread2().start();
    }

线程1遍历时让线程2去修改列表元素。
在这里插入图片描述
我们发现报了并发修改异常,因此可以初步了解fail-fast,就是程序对集合进行迭代时,其它线程对集合进行了结构的修改(比如删除、添加元素)。当检测到对象的并发修改,但只是抛出该异常并不能阻止,有的时候甚至不报异常,所以我们只能把ConcurrentModificationException当做一个信息,不能针对只报了这个异常就只去检查多线程的问题,其它情况也可能会造成集合元素修改。

2.分析

以ArrayList为例,我们看看它如何实现Iterator的

  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;

        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
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

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

我们发现这些方法里面都调用了checkForComodification();
而这个方法里面只有一个if (modCount != expectedModCount)
所以上面为什么并发修改异常的关键就在这里了。
expectedModCount 是在Itr中定义的:

int expectedModCount = ArrayList.this.modCount;

Itr里面的所有方法都未涉及expectedModCount的修改,所以他的值是不可能会变的,那么会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量:

protected transient int modCount = 0;

那它什么时候会改变呢?来接着往下看源码

    public boolean add(E paramE) {
        ensureCapacityInternal(this.size + 1);
        /** 此处省略代码 */
    }
 
    private void ensureCapacityInternal(int paramInt) {
        if (this.elementData == EMPTY_ELEMENTDATA)
            paramInt = Math.max(10, paramInt);
        ensureExplicitCapacity(paramInt);
    }
    
    private void ensureExplicitCapacity(int paramInt) {
        this.modCount += 1;   
        /** 此处省略代码 */
    }
    
   public boolean remove(Object paramObject) {
        int i;
        if (paramObject == null)
            for (i = 0; i < this.size; ++i) {
                if (this.elementData[i] != null)
                    continue;
                fastRemove(i);
                return true;
            }
        else
            for (i = 0; i < this.size; ++i) {
                if (!(paramObject.equals(this.elementData[i])))
                    continue;
                fastRemove(i);
                return true;
            }
        return false;
    }
 
    private void fastRemove(int paramInt) {
        this.modCount += 1; 
        /**  此处省略代码 */
    }
 
    public void clear() {
        this.modCount += 1;  
        /** 此处省略代码 */
    }

因此我们就明白了,集合的结构改变会引起modCount改变从而导致迭代时产生异常。所以fail-fast的原理就出来了:
当使用迭代器遍历集合时,遍历期间对集合本身的结构的修改会导致modCount增大,和expectedModCount 不一致导致报出ConcurrentModificationException

3.解决

目前我了解的有以下两种方案:

  1. 对涉及modCount的地加上synchronized锁或者直接使用 Collection synchronizedList,但是对于加锁操作不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
  2. 使用CopyOnWriteArrayList 替换 ArrayLIst(fail-safe机制),对于map可以使用CurrentHashMap(fail-safe机制)

根据名字也可以看出,CopyOnWriteArrayList是ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是先对底层数组复制一次,然后对复制数组进行操作,这样每个线程操作自己复制的那一份数组,自然就不会产生冲突了。 但是由于需要复制数组所以该类产生的开销比较大,比较适用下面两种情况:

  1. 在不能同步遍历,但又需要从并发线程中排除冲突时。

  2. 当遍历操作的数量大大超过可变操作的数量时。

4.fail-safe 机制

1、fail-safe 任何对集合结构的修改都会在一个复制后的新集合上进行,因此不会产生冲突。

2、fail-safe 机制有两个缺点:

(1)需要复制集合,产生大量的无效对象,开销大。

(2)无法保证读取到的数据是目前原始结构中的数据。

3、使用示例:

    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        Iterator iterator = list.iterator();

        while (iterator.hasNext()) {
            System.out.println(iterator.next());
            list.add(4);
       }
        System.out.println(list.size());
    }

在这里插入图片描述
我们可以看到遍历只输出了1 2 3,而4添加三次后size变成6

这两个机制主要是在hashmap可能会问到,hashtable也是fail-fast的,大家可以再去了解一下。

最后

快速失败其实就是失败后程序抛异常并且停止运行,安全失败就是可以继续运行。他们是一个思想,而不局限于使用在集合中,比如dubbo的集群容错也用到了,如果快速失败就抛异常停止运行,安全失败就打印日志,返回空数据。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值