Maison  >  Article  >  Java  >  Exceptions courantes dans l'accès multithread de Java - perte rapide (échec rapide)

Exceptions courantes dans l'accès multithread de Java - perte rapide (échec rapide)

黄舟
黄舟original
2017-02-28 10:58:301415parcourir


Dans JDK Collection, nous voyons souvent des mots similaires à celui-ci :

Par exemple, ArrayList :

Notez que le comportement d'échec rapide des itérateurs ne peut pas être garanti car en général, il Il n'est pas possible de garantir concrètement si des modifications simultanées désynchronisées se produiront. Les itérateurs à échec rapide lancent ConcurrentModificationException au mieux. Par conséquent, c'est une mauvaise idée d'écrire un programme qui s'appuie sur cette exception pour améliorer l'exactitude de tels itérateurs : le comportement fail-fast des itérateurs ne doit être utilisé que pour détecter des bogues.

Dans HashMap :

Notez que le comportement de défaillance rapide des itérateurs ne peut pas être garanti. De manière générale, lorsqu'il y a des modifications simultanées asynchrones, il est impossible de donner des garanties fermes. Les itérateurs à échec rapide lancent ConcurrentModificationException au mieux. Par conséquent, c'est une erreur d'écrire un programme qui s'appuie sur cette exception ; l'approche correcte est que le comportement rapide des itérateurs doit être utilisé uniquement pour détecter les erreurs du programme.

« Échouer rapidement » est mentionné à plusieurs reprises dans ces deux paragraphes. Alors, quel est le mécanisme du « fail fast » ?

"Fast Failure" est fail-fast, qui est un mécanisme de détection d'erreurs pour les collections Java. Lorsque plusieurs threads effectuent des modifications structurelles dans une collection, un mécanisme d'échec rapide peut se produire. N'oubliez pas que c'est possible, pas certain. Par exemple : supposons qu'il y ait deux threads (Thread 1, Thread 2). Le thread 1 parcourt les éléments de l'ensemble A via Iterator. À un moment donné, le thread 2 modifie la structure de l'ensemble A (c'est une modification de la structure, pas). une simple modification du contenu de l'élément de collection), le programme lancera une exception
ConcurrentModificationException à ce moment-là, générant ainsi un mécanisme d'échec rapide.


1. Exemple d'échec rapide

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



Résultats d'exécution :

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. Causes de l'échec rapide

Grâce aux exemples et explications ci-dessus, je sais au départ que la cause de l'échec rapide est que lorsque le programme itère la collection, un certain thread modifie structurellement la collection. a été modifié, l'itérateur lancera des informations d'exception ConcurrentModificationException, ce qui entraînera un échec rapide.

Pour comprendre le mécanisme de défaillance rapide, nous devons d'abord comprendre l'exception ConcurrentModificationException. Cette exception est levée lorsqu'une méthode détecte une modification simultanée d'un objet mais n'autorise pas de telles modifications. Dans le même temps, il convient de noter que cette exception n'indiquera pas toujours que l'objet a été modifié simultanément par différents threads. Si un seul thread viole les règles, il peut également lever une exception.

Il est vrai que le comportement fail-fast des itérateurs ne peut pas être garanti, et il ne peut pas garantir que cette erreur se produira, mais l'opération fail-fast fera de son mieux pour lancer une ConcurrentModificationException, donc afin de améliorer la précision de telles opérations Ce serait une erreur d'écrire un programme qui s'appuie sur cette exception ; l'approche correcte est la suivante : ConcurrentModificationException ne doit être utilisée que pour détecter les bogues. Ci-dessous, j'utiliserai ArrayList comme exemple pour analyser plus en détail les raisons de l'échec rapide.

D'après le précédent, nous savons que le fail-fast est généré lors de l'utilisation des itérateurs. Jetons maintenant un coup d'œil au code source de l'itérateur dans 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();  
        }  
    }

À partir du code source ci-dessus, nous pouvons voir que l'itérateur appelle next(), La méthode Remove () appelle toujours la méthode checkForComodification(), qui sert principalement à détecter modCount == ExpectModCount? Sinon, une exception ConcurrentModificationException sera levée, créant ainsi un mécanisme d'échec rapide. Donc, pour comprendre pourquoi le mécanisme d'échec rapide se produit, nous devons comprendre pourquoi modCount != ExpectModCount
et quand leurs valeurs ont changé.

ExpectModCount est défini dans Itr : int ExpectModCount = ArrayList.this.modCount ; sa valeur ne peut donc pas être modifiée, donc ce qui va changer est modCount. modCount est défini dans AbstractList et est une variable globale :


protected transient int modCount = 0;

Alors, quand change-t-il et pour quelle raison ? Veuillez consulter le code source d'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)!


Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Article précédent:Fil local Java (ThreadLocal)Article suivant:Fil local Java (ThreadLocal)