ホームページ  >  記事  >  Java  >  Java のマルチスレッド アクセスにおける一般的な例外 -- 高速ロスト (高速失敗)

Java のマルチスレッド アクセスにおける一般的な例外 -- 高速ロスト (高速失敗)

黄舟
黄舟オリジナル
2017-02-28 10:58:301430ブラウズ


JDK コレクションではこれに似た単語がよく見られます:

. 変更には厳密な保証はありません。フェイルファスト反復子は、ベストエフォートベースで ConcurrentModificationException をスローします。したがって、このようなイテレータの正確性を向上させるためにこの例外に依存するプログラムを作成することはお勧めできません。イテレータのフェイルファスト動作はバグを検出するためにのみ使用されるべきです。 bused考える。フェイルファスト反復子は、ベストエフォートベースで ConcurrentModificationException をスローします。したがって、この例外に依存するプログラムを作成するのは間違いであり、イテレータのフェイルファスト動作はプログラム エラーを検出するためだけに使用するのが正しいアプローチです。

「早く失敗せよ」は、この 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() を呼び出すときにイテレータが常に checkForComodification() メソッドを呼び出すことがわかります。このメソッドは主に 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  
    /** 省略此处代码 */  
}  
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)!


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