CopyOnWriteArrayList分析-どのような問題を解決できるか

7034 ワード

CopyOnWriteArrayListで主に解決できる問題は、同時遍歴読み取りロックなし(Iterator経由)である.
CopyOnWriteArrayListとArrayListの比較
もし私たちが頻繁に変化する可能性のあるリスト(配列)を読み取ったら、あなたはどうしますか?
グローバルArrayList(配列)、修正時ロック、読み出し時ロック
読み込み時になぜロックが必要なのでしょうか?
ArrayListが読み取り中にロックされていない場合、他のスレッドがArrayListを変更(追加または削除)すると、failfastメカニズム(ここではIterator遍歴のみを議論し、通常のforループであれば配列が境界を越える可能性がありますが、ここでは議論しません)
配列遍歴読み出しの場合、配列境界が現れる可能性があります
だからリードロックは書く操作です
読み取りとロックがかかると、同時読み取りにはパフォーマンスが悪いに違いありません.もちろん、読み取りと書き込みのロックでこの問題を解決できると言っていますが、ここではロックのない読み取り操作が期待され、スレッドの安全を保証することができます.
以下の例の背景は,比較的高い同時読み取り+比較的低い同時修正である.
List arr = new CopyOnWriteArrayList<>();
//List arr = new ArrayList<>();// ArrayList 
for (int i = 0; i < 3; i++) {
    arr.add(i);
}
// 
for (int i = 0; i < 1000; i++) {
    final int m = i;
    new Thread(() -> {
        try {Thread.sleep(1);} catch (InterruptedException e) {}// 
        Iterator iterator = arr.iterator();
        try {Thread.sleep(new Random().nextInt(10));} catch (InterruptedExcep{}// 
        int count = 0;
        while(iterator.hasNext()){
            iterator.next();
            count++;
        }
        System.out.println("read:"+count);
    }).start();
}
// 
for (int ii = 0; ii < 10; ii++) {
    new Thread(() -> {
        arr.add(123);
        System.out.println("write");
    }).start();
}

上記の例をArrayListに変更すると、次の理由でエラーが発生します.
next()メソッドはcheckForComodificationチェックを呼び出すため、modCount(元のarrayList)とexpectedModCountが一致していないことがわかりました.これは、上記の急速な失敗です.この急速な失敗は、現在合併している状況や問題があるかどうかにかかわらず、一致していないことを発見したら異常を投げます.
ArrayListソリューションではiteratorを巡るときにロックをかける
final void checkForComodification() {
  if (modCount != expectedModCount)
     throw new ConcurrentModificationException();
   }

では、なぜCopyOnWriteArrayListに変えればいいのでしょうか.まずCopyOnWriteを見ないで、CopyOnWriteArrayListのiteratorを分析してみましょう.
public Iterator iterator() {
     return new COWIterator(getArray(), 0);
}

CopyOnWriteArrayListがiteratorを呼び出すと、新しい配列スナップショットが生成されます.遍歴読み取りはスナップショットなので、間違いは報告されません(読み取り後にリストが変更されても)、CopyOnWriteArrayListにはfastfailメカニズムがありません.これは、Iteratorのスナップショット実装およびCopyOnWriteがfastfailによって集合の正確性を保証する必要がないためです.
CopyOnWriteArrayListのCopyOnWrite配列セットを修正すると、配列が再作成されて新しいデータが調整され、調整が完了すると古い配列に新しい配列が割り当てられます
public boolean add(E e) {
    final ReentrantLock lock = this.lock;// 
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;// 
        setArray(newElements);// 
        return true;
    } finally {
        lock.unlock();
    }
}

なぜ新しい配列をコピーするのか、何のメリットがありますか?
新しい配列をコピーしなければ(ロックをかけてもスレッドの安全を保証する)元のデータ構造を直接修正すると、読むときにロックをかけ、ロックをかけなければ修正配列の「半製品」(COWIterator(getArray(),0)を読む可能性があります.半製品です)
新しい配列をコピーして、修正が完了していなくても、遍歴は手に入れたもので古い配列なので、問題はありません.
Doug Lea大神はこのクラスを開発する際にもこのクラスの主な応用シーンを紹介した.集合のiteratorメソッドへのロックを避けることだ.このクラスの注釈の節選を見てみよう.
* making a fresh copy of the underlying array.This is ordinarily too costly, but may be more efficient * than alternatives when traversal operations vastly outnumber * mutations, and is useful when you cannot or don't want to * synchronize traversals, yet need to preclude interference among * concurrent threads. * This array never changes during the lifetime of the * iterator, so interference is impossible and the iterator is * guaranteed not to throw {@code ConcurrentModificationException}. * The iterator will not reflect additions, removals, or changes to * the list since the iterator was created.
大概翻译:
新しい配列をコピーするのは高すぎるように見えますが、変更数をはるかに上回る遍歴は効果的で、synchronized遍歴を使用したくない場合に役立ちます.
この新しいコピーの配列はiteratorライフサイクルでは永遠に変更されず、反復では生C o n c u r r e ntModificationExceptionを異常にしません.
反復器が作成されると、反復器は変更できません(要素を追加、削除)
著者の考えを抽出します.
1、このクラスの使用はスレッドが安全です
2、反復器による同時遍歴ではエラーが報告されず、ロックがない
3、書くことが少なく読むことが多い前提の下で、比較的に適当です