5

次のコードは、ConcurrentModificationExceptionまたはその他の副作用を引き起こしますか?

ArrayList<String> newList = new ArrayList<String>(list);

リストのサイズが非常に大きく、上記のコードが実行されているときに別のスレッドが同時にリストを変更していることを考慮してください。

4

3 に答える 3

9

編集:

ConcurrentModificationException私の最初の応答は「はい」ですが、@ JohnVintが正しく指摘しているように、内部でArrayListSystem.arrayCopy(...). 最後のコード スニペットを参照してください。

問題は、このコピーを行うときに別のスレッドが要素配列に変更を加えていることです。、初期化されていない配列値、またはネイティブ コードで行われるIndexOutOfBoundsExceptionため、ある種のネイティブ メモリ アクセス例外が発生する可能性があります。System.arraycopy(...)

これらの競合状態から保護するために、更新とコピーの両方でリストを同期する必要があります。また、メモリ バリアを確立して、ArrayListバックアップする要素配列が適切に最新であることを確認する必要があります。


public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    ...
}

// ArrayList
public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
}

// Arrays
public static <T,U> T[] copyOf(U[] original, int newLength,
    Class<? extends T[]> newType) {
    ...
    System.arraycopy(original, 0, copy, 0,
        Math.min(original.length, newLength));
}

// System
public static native void arraycopy(Object src,  int  srcPos,
    Object dest, int destPos, int length);
于 2012-06-14T14:02:03.227 に答える
1

ここで何をしているのかを考える必要があります。listのクラスがスレッドセーフでない場合は、このコードを使用して完全に破棄できますlistnewList--a CME は問題が最も少ないでしょう。(CME をスローしないクラスをお勧めしますが、この場合は CME が適しています。) また、このコードはテストが困難です。すべての失敗の間にゼロから 10 億回の問題のない実行が得られ、失敗は非常に微妙である可能性がありますが、それらは大規模で合理的な説明を超えている可能性が高いです。

最も簡単な解決策は、ロックすることlistです。使用する場所はどこでも必ずロックする必要があります。リストを実際にロックしているのではなく、アクセス元のコードブロックをロックしています。すべてのアクセスをロックする必要があります。欠点は、新しいリストの作成中に他のスレッドをブロックすることです。これは本当に行く方法です。ただ、おっしゃる通り「リストが巨大」だと、パフォーマンスが気になるかもしれませんので、続けます…

が不変として扱われ、作成後に頻繁に使用する場合は、これを行う価値がnewListあります。newList多数のコードを問題なく同時に読み取ることができるようになり、矛盾を恐れることもありません。しかし、最初の作成にはまだホールドアップがあります。

次のステップはlist、java.util.ConcurrentLinkedQueue を作成することです。(より洗練されたものが必要な場合は、並行マップとセットがあります。) このことは、さらに多くのスレッドが追加および削除されている間に、それを読み取る多数のスレッドを持つことができ、常に機能します。含まれていると思われるものlist含まれていない可能性がありますが、イテレーターが無限ループに入ることはありません ( java.util.LinkedList の場合に発生する可能性があります)。これによりnewList、他のスレッドが別のコアで動作している間に、あるコアで作成できます。

欠点: listArrayList の場合、並行クラスに切り替えるのは少し手間がかかるかもしれません。並行クラスはより多くのメモリを使用し、通常は ArrayList よりも遅くなります。さらに重要なことは、 の内容listが矛盾している可能性があることです。(実際には、すでにこの問題を抱えています。) 他のスレッドでエントリ A と B を同時に追加または削除し、両方またはどちらもnewList.そこでは、一方が追加または削除された後、もう一方が追加または削除される前にイテレータが通過します。(シングル コア マシンには、この問題はあまりありません。) しかし、listが一定の無秩序な流れの中にあると既に考えられている場合、これはまさにあなたが望むものかもしれません。

別の別の副作用: 大きな配列とそれらを使用するもの (ArrayList や HashTable など) には注意する必要があります。エントリを削除しても使用するスペースが少なくならないため、ほとんどのメモリを占有するデータがほとんどない大きな配列がたくさんできてしまう可能性があります。

さらに悪いことに、エントリを追加すると、古い配列が解放され、新しいより大きな配列が割り当てられるため、空きメモリが断片化されます。つまり、空きメモリの大部分は古い配列から破棄されたチャンクになり、次の割り当てに使用するのに十分な大きさのものはありません。ガベージ コレクターはこれらすべてをデフラグしようとしますが、それは大変な作業であり、GC は空きブロックを再配置するのに時間をかけるよりも、メモリ不足の例外をスローする傾向があります。ちょうど要求した。そのため、メモリの 10% しか使用されていない場合、メモリ不足エラーが発生します。

配列は最も高速ですが、大きな配列は注意して使用する必要があります。それぞれの割り当てを意識して自由に。スペースを再割り当てしないように、適切な初期サイズを指定します。(C プログラマーのふりをしてください。) GC に親切にしましょう。大きなリストを勝手に作成して解放し、サイズを変更する必要がある場合は、LinkedList、TreeMap、ConcurrentLinkedQueue などのリンクされたクラスの使用を検討してください。

于 2012-06-14T16:00:04.513 に答える