Home  >  Article  >  Java  >  Common exceptions in Java's multi-threaded access--fast-lost (fast failure)

Common exceptions in Java's multi-threaded access--fast-lost (fast failure)

黄舟
黄舟Original
2017-02-28 10:58:301430browse


In JDK Collection we often see words similar to this:

For example, ArrayList:

Note that the fast failure behavior of iterators cannot be guaranteed, because In general, it is not possible to make any hard guarantees about whether out-of-sync concurrent modifications will occur. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it is a bad idea to write a program that relies on this exception to improve the correctness of such iterators: the fail-fast behavior of iterators should only be used to detect bugs.

In HashMap:

Note that the fast failure behavior of iterators cannot be guaranteed. Generally speaking, when there are asynchronous concurrent modifications, it is impossible to make any firm guarantees. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it is a mistake to write a program that relies on this exception; the correct approach is that the fail-fast behavior of iterators should be used only to detect program errors.

"Fail fast" is mentioned repeatedly in these two paragraphs. So what is the "fail fast" mechanism?

"Fast failure" is fail-fast, which is an error detection mechanism for Java collections. When multiple threads perform structural changes to a collection, a fail-fast mechanism may occur. Remember it's possible, not certain. For example: Suppose there are two threads (Thread 1, Thread 2). Thread 1 is traversing the elements in set A through Iterator. At some point, thread 2 modifies the structure of set A (it is a modification of the structure, not a simple Modify the content of the collection element), then the program will throw a
ConcurrentModificationException exception at this time, thus generating a fail-fast mechanism.


1. fail-fast example

public class FailFastTest {  
    private static List<Integer> list = new ArrayList<>();  
    /** 
     * @desc:线程one迭代list 
     * @Project:test 
     * @file:FailFastTest.java 
     * @Authro:chenssy 
     * @data:2014年7月26日 
     */  
    private static class threadOne extends Thread{  
        public void run() {  
            Iterator<Integer> iterator = list.iterator();  
            while(iterator.hasNext()){  
                int i = iterator.next();  
                System.out.println("ThreadOne 遍历:" + i);  
                try {  
                    Thread.sleep(10);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    }  
    /** 
     * @desc:当i == 3时,修改list 
     * @Project:test 
     * @file:FailFastTest.java 
     * @Authro:chenssy 
     * @data:2014年7月26日 
     */  
    private static class threadTwo extends Thread{  
        public void run(){  
            int i = 0 ;   
            while(i < 6){  
                System.out.println("ThreadTwo run:" + i);  
                if(i == 3){  
                    list.remove(i);  
                }  
                i++;  
            }  
        }  
    }  
    public static void main(String[] args) {  
        for(int i = 0 ; i < 10;i++){  
            list.add(i);  
        }  
        new threadOne().start();  
        new threadTwo().start();  
    }  
}



Running result:

ThreadOne 遍历:0  
ThreadTwo run:0  
ThreadTwo run:1  
ThreadTwo run:2  
ThreadTwo run:3  
ThreadTwo run:4  
ThreadTwo run:5  
Exception in thread "Thread-0" java.util.ConcurrentModificationException  
    at java.util.ArrayList$Itr.checkForComodification(Unknown Source)  
    at java.util.ArrayList$Itr.next(Unknown Source)  
    at test.ArrayListTest$threadOne.run(ArrayListTest.java:23)


2. fail Reasons for -fast

Through the above examples and explanations, I initially know that the reason for fail-fast is that when the program iterates the collection, a thread makes structural modifications to the collection. , then the iterator will throw ConcurrentModificationException exception information, resulting in fail-fast.

To understand the fail-fast mechanism, we must first understand the ConcurrentModificationException exception. This exception is thrown when a method detects concurrent modification of an object but does not allow such modifications. At the same time, it should be noted that this exception will not always indicate that the object has been modified concurrently by different threads. If a single thread violates the rules, it may also throw an exception.

It is true that the fail-fast behavior of iterators cannot be guaranteed, and it cannot guarantee that this error will definitely occur, but the fail-fast operation will do its best to throw a ConcurrentModificationException, so in order to improve the accuracy of such operations It would be a mistake to write a program that relies on this exception; the correct approach is: ConcurrentModificationException should only be used to detect bugs. Below I will use ArrayList as an example to further analyze the reasons for fail-fast.

We know from the front that fail-fast is generated when operating iterators. Now let's take a look at the source code of the iterator in ArrayList:


private class Itr implements Iterator<E> {  
        int cursor;  
        int lastRet = -1;  
        int expectedModCount = ArrayList.this.modCount;  
        public boolean hasNext() {  
            return (this.cursor != ArrayList.this.size);  
        }  
        public E next() {  
            checkForComodification();  
            /** 省略此处代码 */  
        }  
        public void remove() {  
            if (this.lastRet < 0)  
                throw new IllegalStateException();  
            checkForComodification();  
            /** 省略此处代码 */  
        }  
        final void checkForComodification() {  
            if (ArrayList.this.modCount == this.expectedModCount)  
                return;  
            throw new ConcurrentModificationException();  
        }  
    }

From the above source code we can see that the iterator is calling next(), remove() Methods always call the checkForComodification() method, which mainly detects modCount == expectedModCount? If not, a ConcurrentModificationException exception is thrown, thus creating a fail-fast mechanism. So to figure out why the fail-fast mechanism occurs, we must figure out why modCount != expectedModCount
and when their values ​​changed.

expectedModCount is defined in Itr: int expectedModCount = ArrayList.this.modCount; so its value cannot be modified, so what will change is modCount. modCount is defined in AbstractList and is a global variable:


protected transient int modCount = 0;

So when does it change and for what reason? Please see the source code of ArrayList:


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;    //修改modCount  
    /** 省略此处代码 */  
}  
ublic 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;   //修改modCount  
    /** 省略此处代码 */  
}  
public void clear() {  
    this.modCount += 1;    //修改modCount  
    /** 省略此处代码 */  
}

 

       从上面的源代码我们可以看出,ArrayList中无论add、remove、clear方法只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。所以我们这里可以初步判断由于expectedModCount 得值与modCount的改变不同步,导致两者之间不等从而产生fail-fast机制。知道产生fail-fast产生的根本原因了,我们可以有如下场景:

       有两个线程(线程A,线程B),其中线程A负责遍历list、线程B修改list。线程A在遍历list过程的某个时候(此时expectedModCount = modCount=N),线程启动,同时线程B增加一个元素,这是modCount的值发生改变(modCount + 1 = N + 1)。线程A继续遍历执行next方法时,通告checkForComodification方法发现expectedModCount  = N  ,而modCount
= N + 1,两者不等,这时就抛出ConcurrentModificationException 异常,从而产生fail-fast机制。

       所以,直到这里我们已经完全了解了fail-fast产生的根本原因了。知道了原因就好找解决办法了。


三、fail-fast解决办法

通过前面的实例、源码分析,我想各位已经基本了解了fail-fast的机制,下面我就产生的原因提出解决方案。这里有两种解决方案:

       方案一:在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。

       方案二:使用CopyOnWriteArrayList来替换ArrayList。推荐使用该方案。

       CopyOnWriteArrayList为何物?ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。 该类产生的开销比较大,但是在两种情况下,它非常适合使用。1:在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。2:当遍历操作的数量大大超过可变操作的数量时。遇到这两种情况使用CopyOnWriteArrayList来替代ArrayList再适合不过了。那么为什么CopyOnWriterArrayList可以替代ArrayList呢?

       第一、CopyOnWriterArrayList的无论是从数据结构、定义都和ArrayList一样。它和ArrayList一样,同样是实现List接口,底层使用数组实现。在方法上也包含add、remove、clear、iterator等方法。

       第二、CopyOnWriterArrayList根本就不会产生ConcurrentModificationException异常,也就是它使用迭代器完全不会产生fail-fast机制。请看:

private static class COWIterator<E> implements ListIterator<E> {  
        /** 省略此处代码 */  
        public E next() {  
            if (!(hasNext()))  
                throw new NoSuchElementException();  
            return this.snapshot[(this.cursor++)];  
        }  
        /** 省略此处代码 */  
    }

       CopyOnWriterArrayList的方法根本就没有像ArrayList中使用checkForComodification方法来判断expectedModCount 与 modCount 是否相等。它为什么会这么做,凭什么可以这么做呢?我们以add方法为例:

public boolean add(E paramE) {  
        ReentrantLock localReentrantLock = this.lock;  
        localReentrantLock.lock();  
        try {  
            Object[] arrayOfObject1 = getArray();  
            int i = arrayOfObject1.length;  
            Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);  
            arrayOfObject2[i] = paramE;  
            setArray(arrayOfObject2);  
            int j = 1;  
            return j;  
        } finally {  
            localReentrantLock.unlock();  
        }  
    }  
    final void setArray(Object[] paramArrayOfObject) {  
        this.array = paramArrayOfObject;  
    }

 

       CopyOnWriterArrayList的add方法与ArrayList的add方法有一个最大的不同点就在于,下面三句代码:

Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);  
arrayOfObject2[i] = paramE;  
setArray(arrayOfObject2);

       就是这三句代码使得CopyOnWriterArrayList不会抛ConcurrentModificationException异常。他们所展现的魅力就在于copy原来的array,再在copy数组上进行add操作,这样做就完全不会影响COWIterator中的array了。

       所以CopyOnWriterArrayList所代表的核心概念就是:任何对array在结构上有所改变的操作(add、remove、clear等),CopyOnWriterArrayList都会copy现有的数据,再在copy的数据上修改,这样就不会影响COWIterator中的数据了,修改完成之后改变原有数据的引用即可。同时这样造成的代价就是产生大量的对象,同时数组的copy也是相当有损耗的。

 以上就是Java 的多线程访问常见异常--fast-lost (快速失败 ) 的内容,更多相关内容请关注PHP中文网(www.php.cn)!


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn