14

HashMap の javadocには次のように記載されています。

イテレータの作成後に、イテレータ自体の remove メソッド以外の方法でマップが構造的に変更された場合、イテレータは ConcurrentModificationException をスローします。

仕様に基づいて、ほぼ即座に失敗し、ConcurrentModificationException をスローするサンプル コードを作成しました。

  • Java 7 では予想どおりすぐに失敗します
  • しかし、Java 6 では常に動作するようです (つまり、約束された例外をスローしません)。

注: Java 7 では失敗しない場合があります (20 回のうち 1 回など) - スレッドのスケジューリングに関係していると思います (つまり、2 つのランナブルがインターリーブされていません)。

何か不足していますか?Java 6 で実行されるバージョンが ConcurrentModificationException をスローしないのはなぜですか?

実際には、2 つの Runnable タスクが並行して実行されています (ほぼ同時に開始するためにカウントダウンラッチが使用されます)。

  • 1つはマップにアイテムを追加することです
  • もう1つはマップを反復処理し、キーを読み取って配列に入れます

次に、メイン スレッドは、配列に追加されたキーの数をチェックします。

Java 7 の典型的な出力(反復はすぐに失敗します):

java.util.ConcurrentModificationException
MAX i = 0

Java 6の典型的な出力 (反復全体が実行され、配列には追加されたすべてのキーが含まれます):

最大 i = 99

使用コード:

public class Test1 {

    public static void main(String[] args) throws InterruptedException {
        final int SIZE = 100;
        final Map<Integer, Integer> map = new HashMap<Integer, Integer>();
        map.put(1, 1);
        map.put(2, 2);
        map.put(3, 3);
        final int[] list = new int[SIZE];
        final CountDownLatch start = new CountDownLatch(1);
        Runnable put = new Runnable() {
            @Override
            public void run() {
                try {
                    start.await();
                    for (int i = 4; i < SIZE; i++) {
                        map.put(i, i);
                    }
                } catch (Exception ex) {
                }
            }
        };

        Runnable iterate = new Runnable() {
            @Override
            public void run() {
                try {
                    start.await();
                    int i = 0;
                    for (Map.Entry<Integer, Integer> e : map.entrySet()) {
                        list[i++] = e.getKey();
                        Thread.sleep(1);
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        };
        ExecutorService e = Executors.newFixedThreadPool(2);
        e.submit(put);
        e.submit(iterate);
        e.shutdown();

        start.countDown();
        Thread.sleep(100);
        for (int i = 0; i < SIZE; i++) {
            if (list[i] == 0) {
                System.out.println("MAX i = " + i);
                break;
            }
        }
    }
}

注: x86 マシンで JDK 7u11 および JDK 6u38 (64 ビット バージョン) を使用します。

4

3 に答える 3

9

ソースを調べてHashMap、Java 6 と Java 7 の間でそれらを比較すると、興味深い違いが見られます。

transient volatile int modCount;Java6 と Java7 だけtransient int modCount;です。

これが原因で、言及されたコードの異なる動作の原因であると確信しています:

        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();

UPD:これは Java 6/7 の既知のバグであると思われます: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6625725最新の Java7 で修正されました。

UPD-2: @Renjith 氏は、テストしたばかりで、HashMaps 実装の動作に違いは見られなかったと述べました。しかし、私もそれをテストしました。

私のテストは:

1) Java 6HashMap2の完全なコピーであるクラスを作成しました。HashMap

重要なことの 1 つは、ここで 2 つの新しいフィールドを導入する必要があることです。

transient volatile Set<K>        keySet = null;

transient volatile Collection<V> values = null;

2)次にHashMap2、この質問のテストでこれを使用し、 Java 7で実行しました

結果:の下のテストのように機能します。Java 6つまり、何もありませんConcurentModificationException

以上が私の推測の証明です。QED

于 2013-01-16T16:50:10.140 に答える
6

補足として、ConcurrentModificationException(残念な名前にもかかわらず) は、複数のスレッドにまたがる変更を検出するためのものではありません。単一のスレッド内で変更をキャッチすることのみを目的としています複数のスレッド間で共有 HashMap を変更した場合 (正しい同期なし) の効果は、イテレーターやその他のものの使用に関係なく、壊れることが保証されています。

要するに、あなたのテストはjvmのバージョンに関係なく偽物であり、まったく違うことをするのは「運」だけです。たとえば、このテストでは、クロス スレッドで表示したときに HashMap の内部が一貫性のない状態にあるため、NPE またはその他の「不可能な」例外がスローされる可能性があります。

于 2013-01-16T17:00:40.757 に答える
1

私の理論では、Java 6 と 7 の両方で、リーダー スレッドでイテレータを作成すると、ライター スレッドに 100 エントリを配置するよりも時間がかかります。これは主に、新しいクラスをロードして初期化する必要があるためです (EntrySet, AbstractSet, AbstractCollection, Set, EntryIterator, HashIterator, Iteratorつまり

したがって、この行がリーダー スレッドで実行されるまでに、ライター スレッドは終了しています。

    HashIterator() {
        expectedModCount = modCount;
        if (size > 0) { // advance to first entry

Java 6 ではmodCountis以降volatile、イテレータは最新のを参照modCount and sizeするため、残りの反復はスムーズに進みます。

Java 7 では、modCountは volatile ではありません。イテレータはおそらく stale を認識しmodCount=3, size=3ます。の後sleep(1)、イテレータは updatedmodCountを確認し、すぐに失敗します。

この理論のいくつかの欠点:

  1. 理論はMAX i=1Java 7で予測する必要があります
  2. main() が実行される前に、HashMapおそらく他のコードによって反復されたため、言及されたクラスはおそらく既にロードされていました。
  3. リーダー スレッドが古くなったと見なすmodCount可能性はありますが、そのスレッドでの変数の最初の読み取りであるため、可能性は低いです。以前にキャッシュされた値はありません。

Hashmap にロギング コードを植えて、リーダー スレッドが何を見ているかを調べることで、この問題の真相を突き止めることができるかもしれません。

于 2013-01-16T17:50:04.717 に答える