ホームページ  >  記事  >  Java  >  Java 改良章 (34)-----フェイルファスト機構

Java 改良章 (34)-----フェイルファスト機構

黄舟
黄舟オリジナル
2017-02-11 10:25:381403ブラウズ

O JDK のコレクションでは、次のような単語がよく見られます:

、たとえば ArrayList:

一般に、いいえ、いいえ、可能性があるため、反復子の急速な失敗は保証できないことに注意してください。非同期の同時変更が発生するかどうかについては、厳密な保証はありません。フェイルファスト反復子は、ベストエフォートベースで ConcurrentModificationException をスローします。したがって、このようなイテレータの正確性を向上させるためにこの例外に依存するプログラムを作成することはお勧めできません。イテレータのフェイルファスト動作はバグを検出するためにのみ使用されるべきです。一般的に、非同期同時実行の変更がある場合、反復子の急速な失敗は保証できないことに注意してください。保証します。フェイルファスト反復子は、ベストエフォートベースで ConcurrentModificationException をスローします。したがって、この例外に依存するプログラムを作成するのは間違いであり、イテレータのフェイルファスト動作はプログラム エラーを検出するためだけに使用するのが正しいアプローチです。 「Fail fast」は、この 2 つの段落で繰り返し言及されています。では、「フェイルファスト」メカニズムとは何でしょうか? 「フェイルファスト」とはフェイルファストのことで、Java コレクションのエラー検出メカニズムです。複数のスレッドがコレクションの構造変更を実行すると、フェイルファスト メカニズムが発生する可能性があります。可能性はあるが、確実ではないことを覚えておいてください。例: 2 つのスレッド (スレッド 1、スレッド 2) があるとします。スレッド 1 はイテレーターを介してセット A の要素を走査しています。ある時点で、スレッド 2 がセット A の構造を変更します (これは構造の変更ではありません)。コレクション要素の内容を単純に変更する場合)、プログラムはこの時点で ConcurrentModificationException 例外をスローし、フェイルファスト メカニズムを生成します。

1. フェイルファストの例

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

実行結果:

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. フェイルファストの原因

上記の例と説明から、フェイルファストの理由は、プログラムがコレクションを反復するときにスレッドがコレクションを構造的に変更し、その結果反復子が ConcurrentModificationException 情報をスローしてフェイルファストが発生するためであることが最初にわかりました。

フェイルファストメカニズムを理解するには、まず ConcurrentModificationException 例外を理解する必要があります。この例外は、メソッドがオブジェクトの同時変更を検出したが、そのような変更を許可しない場合にスローされます。同時に、この例外は、オブジェクトが異なるスレッドによって同時に変更されたことを常に示すわけではなく、単一のスレッドがルールに違反した場合にも例外をスローする可能性があることに注意してください。

イテレータのフェイルファスト動作は保証できないのは事実ですが、このエラーが発生することは保証できませんが、フェイルファスト操作は ConcurrentModificationException 例外をスローするために最善を尽くします。このような操作の正確性を向上させるために、この例外に依存するプログラムを作成するのは間違いです。正しいアプローチは次のとおりです。ConcurrentModificationException はバグを検出するためにのみ使用する必要があります。以下では、フェイルファストの理由をさらに分析するための例として ArrayList を使用します。 前から、イテレーターを操作するときにフェイルファストが生成されることがわかりました。次に、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();
        }
    }

上記のソース コードから、 next() メソッドと Remove() メソッドを呼び出すときにイテレータが呼び出されることがわかります。 () メソッドでは、このメソッドは主に modCount == ExpectedModCount? を検出するためのもので、そうでない場合は ConcurrentModificationException 例外がスローされ、フェイルファスト メカニズムが作成されます。したがって、フェイルファストメカニズムが発生する理由を理解するには、modCount != ExpectedModCount の理由と、それらの値がいつ変更されたかを理解する必要があります。

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


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。