Maison >Java >javaDidacticiel >Chapitre d'amélioration Java (34) ----- mécanisme d'échec rapide

Chapitre d'amélioration Java (34) ----- mécanisme d'échec rapide

黄舟
黄舟original
2017-02-11 10:25:381440parcourir

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

Par exemple, ArrayList :

Notez que le comportement rapide des itérateurs n'est pas garanti, car en général, il n'est pas possible de garantir de manière concrète si des modifications simultanées non 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.

HashMap :

Notez que le comportement fail-fast des itérateurs n'est pas garanti ,D'une manière générale, il n'est pas possible de donner des garanties fermes lorsqu'il y a des modifications simultanées asynchrones. 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.

Mentionné à plusieurs reprises "échec rapide" dans ces deux mots. Alors, quel est le mécanisme du « fail fast » ?

« Échec rapide » est un échec rapide, 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 alors 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 thread Lorsque la structure est 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.

Certes, le comportement fail-fast des itérateurs n'est pas garanti, cela ne garantit pas que cette erreur se produira, mais l'opération fail-fast fera son Il est préférable de lancer une exception ConcurrentModificationException, c'est donc une erreur d'écrire un programme qui s'appuie sur cette exception pour améliorer l'exactitude de telles opérations. 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();
        }
    }

                                          Nous pouvons voir à partir de le code que l'itérateur appelle la méthode checkForComodification() lors de l'appel des méthodes next() et remove(). Cette méthode sert principalement à détecter modCount == ExpectModCount? Sinon, une exception ConcurrentModificationException sera levée, ce qui entraînera un mécanisme d'échec rapide. Donc, pour comprendre pourquoi le mécanisme de défaillance rapide se produit, nous devons comprendre pourquoi modCount != ExpectModCount et quand leurs valeurs ont changé.

        expectedModCount 是在Itr中定义的:int expectedModCount = ArrayList.this.modCount;所以他的值是不可能会修改的,所以会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量:

protected transient int modCount = 0;

那么他什么时候因为什么原因而发生改变呢?请看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
        /** 省略此处代码 */
    }
    
   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;   //修改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提高篇(三四)-----fail-fast机制的内容,更多相关内容请关注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