• 快速失败机制--fail-fast


    fail-fast 机制是Java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast(快速失败)事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

    迭代器的快速失败行为无法得到保证,它不能保证一定会出现该错误,但是快速失败操作会尽最大努力抛出ConcurrentModificationException异常。

    注意:上面所说的是在多线程环境下会发生fail-fast事件,但是单线程条件下如果违反了规则也是会产生fail-fast事件的

    在文档中有这么一段话:编写的程序依赖于快速失败机制产生的异常是不对的,迭代器的快速检测机制仅仅用于检测错误。

    分别用两段程序测试快速失败机制产生的原因:

    单线程环境:
    import java.util.ArrayList;
    import java.util.Iterator;
    import java.util.List;
    
    /**
     * Created with IDEA
     *
     * @author DuzhenTong
     * @Date 2018/3/18
     * @Time 17:34
     */
    public class FailFast {
        public static void main(String[] args) {
            List list = new ArrayList();
            for (int i = 0; i < 10; i++) {
                list.add(i);
            }
            iterator(list);
        }
    
        public static void iterator(List list) {
            Iterator it = list.iterator();
            int index = 0;
            while (it.hasNext()) {
                if (index == 6) {
                    list.remove(index);
                }
                index++;
                System.out.println(it.next());
            }
        }
    }
    

    输出结果:

    0
    1
    2
    3
    4
    5
    Exception in thread "main" java.util.ConcurrentModificationException
    	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:819)
    	at java.util.ArrayList$Itr.next(ArrayList.java:791)
    	at FailFast.iterator(FailFast.java:29)
    	at FailFast.main(FailFast.java:18)
    
    多线程环境:
    import java.util.ArrayList;
    import java.util.Iterator;
    import java.util.List;
    
    /**
     * Created with IDEA
     *
     * @author DuzhenTong
     * @Date 2018/3/18
     * @Time 17:59
     */
    public class FailFast1 {
    
        public static List list = new ArrayList();
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                list.add(i);
            }
            new ThreadA().start();
            new ThreadB().start();
        }
    
        public static class ThreadA extends Thread {
            @Override
            public void run() {
                Iterator it = list.iterator();
                while (it.hasNext()) {
                    System.out.println("集合遍历中……:"+it.next());
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        public static class ThreadB extends Thread {
            @Override
            public void run() {
                int index = 0;
                while (index != 10) {
                    System.out.println("线程等待中……:"+index);
                    if (index == 3) {
                        list.remove(index);
                    }
                    index++;
                }
            }
        }
    }
    

    输出结果:

    线程等待中……:0
    集合遍历中……:0
    线程等待中……:1
    线程等待中……:2
    线程等待中……:3
    线程等待中……:4
    线程等待中……:5
    线程等待中……:6
    线程等待中……:7
    线程等待中……:8
    线程等待中……:9
    Exception in thread "Thread-0" java.util.ConcurrentModificationException
    	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:819)
    	at java.util.ArrayList$Itr.next(ArrayList.java:791)
    	at FailFast1$ThreadA.run(FailFast1.java:28)
    

    上面的程序已经说明了为什么会发生fail-fast事件(快速失败),在多线程条件下,一个线程正在遍历集合中的元素,这时候另一个线程更改了集合的结构,程序才会抛出ConcurrentModificationException,在单线程条件下也是在遍历的时候,这时候更改集合的结构,程序就会抛出ConcurrentModificationException。
    要具体知道为什么会出现fail-fast,就要分析源码,fail-fast出现是在遍历集合的时候出现的,也就是对集合进行迭代的时候,对集合进行迭代的时候都是操作迭代器,集合中的内部类:(ArrayList源码)

    private class Itr implements Iterator<E> {
            int cursor;       
            int lastRet = -1; 
            int expectedModCount = modCount;//---------------------1
    
            public boolean hasNext() {
                return cursor != size;
            }
    
            @SuppressWarnings("unchecked")
            public E next() {
                checkForComodification();
                //………此处代码省略…………
            }
    
            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();
                }
            }
    
            final void checkForComodification() {
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
            }
        }
    

    上面的代码中我们可以看到抛出ConcurrentModificationException异常,上面的next(),remove()都会调用checkForComodification()方法检查两个变量的值是否相等,不相等就会抛出异常。在上面程序中的数字1处:

    int expectedModCount = modCount;
    

    modCount的值赋值给expectedModCount,知道modCount这个值的含义是什么?为什么会发生改变?原因就会找到了。
    源码点进去,发现这个modCount变量并不在ArrayList类中,而在AbstractList中,作为一个成员变量。

    protected transient int modCount = 0;
    

    接下来分析源码,看最常用的方法

    ArrayList中add方法:
    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    

    调用的ensureCapacityInternal方法:

    private void ensureCapacityInternal(int minCapacity) {
            modCount++;//modCount自增————————————
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
    
    ArrayList中的remove方法:
    public E remove(int index) {
            rangeCheck(index);
    
            modCount++;//modCount自增——————————————
            E oldValue = elementData(index);
    
            int numMoved = size - index - 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            elementData[--size] = null; // Let gc do its work
    
            return oldValue;
        }
    
    ArrayList中的clear方法:
    public void clear() {
            modCount++;//modCount自增——————————————
    
            // Let gc do its work
            for (int i = 0; i < size; i++)
                elementData[i] = null;
    
            size = 0;
        }
    

    分析了源码就知道原因是什么了,凡是涉及到改变了集合的结构(改变元素的个数)的操作(包括增加,移除或者清空等)modCount这个变量都会自增,在获得迭代对象的时候,先把这个modCount变量赋值给expectedModCount,在迭代的时候每次都会检查这个变量是否与expectedModCount一致,因为如果是在集合中添加或者删除元素modCount的值都会发生改变。

    解决方法:

    • 对于涉及到更改集合中元素个数的操作通通加上synchronized,或者利用Collections.synchronizedList强制他们的操作都是同步的。
    • 使用CopyOnWriteArrayList来替换ArrayList

    这里在网上看到很多的文章都是这么说的:为什么CopyOnWriteArrayList可以做到不会发生fail-fast?因为CopyOnWriteArrayList所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。

    可以分析源码(下面的1,2处)

    public boolean add(E e) {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                Object[] elements = getArray();//————————1
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len + 1);//————————2
                newElements[len] = e;
                setArray(newElements);
                return true;
            } finally {
                lock.unlock();
            }
        }
    

    我的看法:原因不止有CopyOnWriteArrayList的add、set、remove等会改变原数组的方法中,都是先copy一份原来的array,再在copy数组上进行add、set、remove操作,这就才不影响COWIterator那份数组。

    为什么没有记录修改次数的值或者说不比较modCount也能做到内存的一致性呢?
    在上面的代码1处,调用了getArray()方法,看源码:

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

    因为getArray()返回的array的类型是用volatile修饰的,volatile类型的(强制内存一致性)
    具体可以看我的另一篇关于volatile的:volatile关键字解析

    参考文章:http://cmsblogs.com/?p=1220

  • 相关阅读:
    数数小木块
    Triangular Sums
    Financial Management
    阶乘因式分解(一)
    另一种阶乘问题
    韩信点兵
    Fibonacci数
    A+B Problem
    16进制的简单运算
    浅谈数据库之事务
  • 原文地址:https://www.cnblogs.com/duzhentong/p/8597512.html
Copyright © 2020-2023  润新知