Heim >Java >javaLernprogramm >Häufige Ausnahmen beim Multithread-Zugriff von Java – fast-lost (schneller Fehler)

Häufige Ausnahmen beim Multithread-Zugriff von Java – fast-lost (schneller Fehler)

黄舟
黄舟Original
2017-02-28 10:58:301467Durchsuche


In der JDK-Sammlung sehen wir häufig Wörter wie diese:

Zum Beispiel ArrayList:

Beachten Sie, dass das schnelle Fehlerverhalten von Iteratoren nicht garantiert werden kann, da im Allgemeinen Es ist nicht möglich, verbindliche Garantien dafür zu geben, ob nicht synchronisierte gleichzeitige Änderungen auftreten. Fail-Fast-Iteratoren lösen ConcurrentModificationException auf Best-Effort-Basis aus. Daher ist es keine gute Idee, ein Programm zu schreiben, das auf dieser Ausnahme basiert, um die Korrektheit solcher Iteratoren zu verbessern: Das Fail-Fast-Verhalten von Iteratoren sollte nur zum Erkennen von Fehlern verwendet werden.

In HashMap:

Beachten Sie, dass das schnelle Fehlerverhalten von Iteratoren nicht garantiert werden kann. Im Allgemeinen ist es bei asynchronen gleichzeitigen Änderungen nicht möglich, feste Garantien zu geben. Fail-Fast-Iteratoren lösen ConcurrentModificationException auf Best-Effort-Basis aus. Daher ist es ein Fehler, ein Programm zu schreiben, das auf dieser Ausnahme basiert. Der richtige Ansatz besteht darin, das Fail-Fast-Verhalten von Iteratoren nur zur Erkennung von Programmfehlern zu verwenden.

„Schnell scheitern“ wird in diesen beiden Absätzen immer wieder erwähnt. Was ist also der „Fail-Fast“-Mechanismus?

„Fast Failure“ ist Fail-Fast, ein Fehlererkennungsmechanismus für Java-Sammlungen. Wenn mehrere Threads strukturelle Änderungen an einer Sammlung vornehmen, kann ein Fail-Fast-Mechanismus auftreten. Denken Sie daran, es ist möglich, aber nicht sicher. Beispiel: Angenommen, es gibt zwei Threads (Thread 1, Thread 2). Irgendwann ändert Thread 2 die Struktur von Set A (es handelt sich um eine Modifikation der Struktur). ein einfaches Ändern des Inhalts des Sammlungselements), dann löst das Programm zu diesem Zeitpunkt die Ausnahme
ConcurrentModificationException aus und generiert so einen Fail-Fast-Mechanismus.


1. Fail-Fast-Beispiel

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();  
    }  
}



Laufergebnisse:

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. Ursachen für Fail-Fast

Anhand der obigen Beispiele und Erklärungen weiß ich zunächst, dass die Ursache für Fail-Fast darin liegt, dass ein bestimmter Thread die Sammlung strukturell ändert geändert wurde, löst der Iterator ConcurrentModificationException-Ausnahmeinformationen aus, was zu einem Fail-Fast führt.

Um den Fail-Fast-Mechanismus zu verstehen, müssen wir zunächst die ConcurrentModificationException-Ausnahme verstehen. Diese Ausnahme wird ausgelöst, wenn eine Methode eine gleichzeitige Änderung eines Objekts erkennt, diese Änderung jedoch nicht zulässt. Gleichzeitig ist zu beachten, dass diese Ausnahme nicht immer darauf hinweist, dass das Objekt gleichzeitig von verschiedenen Threads geändert wurde. Wenn ein einzelner Thread gegen die Regeln verstößt, kann es auch zu einer Ausnahme kommen.

Es ist wahr, dass das Fail-Fast-Verhalten von Iteratoren nicht garantiert werden kann und es nicht garantiert werden kann, dass dieser Fehler auftritt, aber der Fail-Fast-Vorgang wird sein Bestes tun, um eine ConcurrentModificationException auszulösen Verbessern Sie die Genauigkeit solcher Operationen. Es ist ein Fehler, ein Programm zu schreiben, das auf dieser Ausnahme basiert. Der richtige Ansatz ist: ConcurrentModificationException sollte nur zum Erkennen von Fehlern verwendet werden. Im Folgenden werde ich ArrayList als Beispiel verwenden, um die Gründe für Fail-Fast weiter zu analysieren.

Aus dem Vorhergehenden wissen wir, dass beim Betrieb von Iteratoren ein Fail-Fast generiert wird. Schauen wir uns nun den Quellcode des Iterators in ArrayList an:


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();  
        }  
    }

Aus dem obigen Quellcode können wir erkennen, dass der Iterator next() aufruft. Die Methode „remove()“ ruft immer die Methode „checkForComodification()“ auf, die hauptsächlich dazu dient, „modCount == erwartetModCount?“ zu erkennen. Wenn nicht, wird eine ConcurrentModificationException-Ausnahme ausgelöst, wodurch ein Fail-Fast-Mechanismus erstellt wird. Um also herauszufinden, warum der Fail-Fast-Mechanismus auftritt, müssen wir herausfinden, warum modCount != erwartetModCount
und wann sich ihre Werte geändert haben.

„expectedModCount“ ist in Itr definiert: int „expectedModCount = ArrayList.this.modCount;“, sodass sein Wert nicht geändert werden kann. Was sich also ändert, ist „modCount“. modCount ist in AbstractList definiert und eine globale Variable:


protected transient int modCount = 0;

Wann ändert es sich also und aus welchem ​​Grund? Bitte sehen Sie sich den Quellcode von ArrayList an:


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)!


Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn