ArrayListがjava.util.ConcurrentModificationExceptionを報告するのはどの場合ですか?

ArrayListは非常に一般的に使用されるコレクションクラスであり、最下層は配列ですが、ArrayListは配列をカプセル化し、add()メソッド、size()メソッド、get()メソッド、および一連のメソッドなどの便利なメソッドを実装し、配列の動的拡張を実装します。
new ArrayList();空の配列が作成され、その容量は0から始まります。ArrayListの初期容量はインタビュー中にあると多くの人が言うのはなぜ10ですか?その理由は、add()メソッドを初めて呼び出して要素を追加するときに、展開が行われるためです。この時点で、容量はデフォルトの初期容量に拡張されます10
。定数はArrayList DEFAULT_CAPACITYで次のように定義されています:
ここに画像の説明を挿入
ArrayListのソースコードの解釈については、前に書いたブログを参照してください:
ArrayList、LinkedList、HashMap、HashSet -----ソースコードの解釈配列リスト

この記事では主に、同時変更例外の発生、つまりマルチスレッドの同時呼び出しadd()メソッドの呼び出しについて説明します

以下はテストコードの一部です

ArrayList<String> arrayList = new ArrayList();
for (int i = 0; i < 2000; i++) {
    
    //如果没有出现并发修改异常则讲循环调大点
    int finalI = i;
    new Thread(() -> {
    
    
        arrayList.add("" + finalI);
    }, "thread:" + i).start();

}

arrayList.stream().forEach(System.out::println);//遍历ArrayList进行打印

以下は、実行されたコードの出力です
ここに画像の説明を挿入

疑問がありますか?なぜ同時変更例外があるのですか?この例外はどのようにスローされましたか?

疑問を解決する

この例外の場所を見つけるために、私と一緒にソースコードを追跡してみましょう。

これをadd()行い、debugデバッグするメソッドにブレークポイントを追加しました

次のコードを実行します

public boolean add(E e) {
    
    
    modCount++; //注意这个参数,是来自父类的成员变量 AbstractList
    add(e, elementData, size);//近一步调用三个参数的add方法
    return true;//表示添加成功
}
private void add(E e, Object[] elementData, int s) {
    
    
    if (s == elementData.length) //判断是否需要扩容
        elementData = grow();//进行扩容,将扩容后的新数组重新赋值给 elementData
    elementData[s] = e; //将元素添加进数组中
    size = s + 1; // s是下标,size是实际的元素个数,因此需要 数组下标 +1 = 元素个数
}

addメソッドにはjava.util.ConcurrentModificationException例外に関する情報がないように見えます。
単に要素を追加し、長さが十分でない場合はgrow()拡張します。
それgrow()このメソッドでスローされた例外でしょうか?さらに追跡するgrow()

private Object[] grow() {
    
    
    return grow(size + 1); //调用了带参数的 grow方法
}

同時変更の例外に関するコードがないことがわかります。

private Object[] grow(int minCapacity) {
    
    
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    
    
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
    
    
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

Arrays copyOf()メソッドを呼び出して要素をコピーし、新しい配列を取得して戻ります。
また、同時変更例外コードもありません。では、どこで例外がスローされるのでしょうか?

public static <T> T[] copyOf(T[] original, int newLength) {
    
    
    return (T[]) copyOf(original, newLength, original.getClass());
}

例外はadd()メソッドでスローされた例外ではなく、後でトラバーサル印刷によってスローされた例外です。
トラバーサルを入力してください。もちろん、テストコードはjava8ストリーム操作を使用して印刷します。
以前に残っているメンバー変数があります。modCount++このメンバー変数は自己インクリメントされます。この変数はどこから取得されますか?トレースします。親クラスAbstarctListに関しては、このメンバー変数が変更カウントの記録に使用されていることも名前から知ることができます。
このカテゴリを検索すると、ConcurrentModificationException11件の引用箇所(コメントを含む)が見つかりました。それを1つずつ見て、内部クラスで参照される最初の場所を見つけましたItr。このクラスは以前に分析されており、コレクションの反復処理に使用されています。つまり、反復走査中にmodCount値が期待値と異なることを検出し、この例外をスローします。
この結果の理由は、add()これmodCount呼び出すマルチスレッドが増加するためです。
次に、スレッドは処理を完了していません。次に、反復を呼び出して要素を取得します。modCountカウントが期待されたものexpectedModCountと等しくないことが判明したため、例外がスローされます

以下はItr、クラスで定義されたメソッドと例外をスローするための条件です
final void checkForComodification() {
    
    
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

Itrこのメソッドは、この内部クラスの2つの場所、つまり
①でremove()
呼び出されます。next()
つまり、これらの2つのメソッドは、java.util.ConcurrentModificationException

加えてItr、このクラスが存在ConcurrentModificationExceptionしているListItrとも呼び出しの内部クラスそれぞれ
previous()
set(E e)
次の検出方法がで定義されたadd(E e)
内部クラスRandomAccessSpliterator

static void checkAbstractListModCount(AbstractList<?> alist, int expectedModCount) {
    
    
    if (alist != null && alist.modCount != expectedModCount) {
    
    
        throw new ConcurrentModificationException();
    }
}

呼ばれる方法である
tryAdvance(Consumer<? super E> action)
forEachRemaining(Consumer<? super E> action)

<E> E get(List<E> list, int i)方法直接例外をスローする。
例外をスローすることができる最後の場所は、静的な内部クラスでありSubList<E>
同時変更例外を検出する方法を定義します。

private void checkForComodification() {
    
    
    if (root.modCount != this.modCount)
        throw new ConcurrentModificationException();
}

では
E set(int index, E element)
E get(int index)
int size()
void add(int index, E element)
E remove(int index)
oid removeRange(int fromIndex, int toIndex)
addAll(int index, Collection<? extends E> c)
ListIterator<E> listIterator(int index)
上記のすべての呼び出し、checkForComodification()同時変更例外がスローされます。
つまり、これらのメソッドが呼び出されると、java.util.ConcurrentModificationException例外がスローされる可能性があります

java.util.ConcurrentModificationException例外がテストコードでスローされる理由は、要素の反復取得が呼び出され、ArrayList中的forEach方法
着信がコンシューマインターフェイスであるためです。

 @Override
 public void forEach(Consumer<? super E> action) {
    
    
     Objects.requireNonNull(action);
     final int expectedModCount = modCount;
     final Object[] es = elementData;
     final int size = this.size;
     for (int i = 0; modCount == expectedModCount && i < size; i++)
         action.accept(elementAt(es, i));
     if (modCount != expectedModCount)
         throw new ConcurrentModificationException();
 }

後者modCount != expectedModCountのif判定条件で条件が成立するため、同時変更例外が発生
し、テストケースのフローによりforEach同時変更例外が発生ます。

総括する:

ArrayListのadd(E e)メソッドは並行性の問題を引き起こしますが、java.util.ConcurrentModificationException例外を報告しませんが、トラバースするときは。たとえば、テストコードでforEach例外スローします並行処理の例外は発生するはずがありませんが、ArrayListには並行処理の問題があります。

つまり、使用するスレッドの数に関係なく、add(E e)例外はスローされません同じ配列の位置に繰り返し書き込みが行われることは事実ですが、どのスレッドが同時例外を引き起こしたかはわかりません。

おすすめ

転載: blog.csdn.net/qq_41813208/article/details/107768960