1. 概要

このチュートリアルでは、 Collections.synchronizedMap() ConcurrentHashMapの違いについて説明します。

さらに、それぞれの読み取りおよび書き込み操作のパフォーマンス出力を確認します。

2. 違い

Collections.synchronizedMap() ConcurrentHashMap はどちらも、データのコレクションに対してスレッドセーフな操作を提供します。

Collections ユーティリティクラスは、コレクションを操作してラップされたコレクションを返すポリモーフィックアルゴリズムを提供します。 そのsynchronizedMap()メソッドは、スレッドセーフな機能を提供します。

名前が示すように、 synchronizedMap()は、パラメーターで指定したMapに基づく同期されたMapを返します。 スレッドセーフを提供するために、 synchronizedMap()は、返されたMapを介したバッキングMapへのすべてのアクセスを許可します。

ConcurrentHashMap は、検索と更新の高い同時実行性をサポートするHashMapの拡張機能としてJDK1.5で導入されました。 HashMap はスレッドセーフではないため、スレッドの競合中に誤った結果が生じる可能性があります。

ConcurrentHashMapクラスはスレッドセーフです。 したがって、複数のスレッドを1つのオブジェクトで問題なく操作できます。

ConcurrentHashMapでは、読み取り操作は非ブロッキングですが、書き込み操作は特定のセグメントまたはバケットをロックします。デフォルトのバケットまたは同時実行レベルは16です。つまり、16スレッドは、セグメントまたはバケットをロックします。

2.1. ConcurrentModificationException

HashMap のようなオブジェクトの場合、同時操作の実行は許可されていません。 したがって、反復中に HashMap を更新しようとすると、ConcurrentModificationExceptionが発生します。 これは、 synchronizedMap()を使用している場合にも発生します。

@Test(expected = ConcurrentModificationException.class)
public void whenRemoveAndAddOnHashMap_thenConcurrentModificationError() {
    Map<Integer, String> map = new HashMap<>();
    map.put(1, "baeldung");
    map.put(2, "HashMap");
    Map<Integer, String> synchronizedMap = Collections.synchronizedMap(map);
    Iterator<Entry<Integer, String>> iterator = synchronizedMap.entrySet().iterator();
    while (iterator.hasNext()) {
        synchronizedMap.put(3, "Modification");
        iterator.next();
    }
}

ただし、これはConcurrentHashMapには当てはまりません。

Map<Integer, String> map = new ConcurrentHashMap<>();
map.put(1, "baeldung");
map.put(2, "HashMap");
 
Iterator<Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    map.put(3, "Modification");
    iterator.next()
}
 
Assert.assertEquals(3, map.size());

2.2. nullサポート

Collections.synchronizedMap() ConcurrentHashMapはnullキーと値を異なる方法で処理します

ConcurrentHashMap は、キーまたは値にnullを許可しません。

@Test(expected = NullPointerException.class)
public void allowNullKey_In_ConcurrentHasMap() {
    Map<String, Integer> map = new ConcurrentHashMap<>();
    map.put(null, 1);
}

でも、 Collections.synchronizedMap()を使用する場合、nullサポートは入力マップに依存します私たちは1つを持つことができますヌルキーおよび任意の数としてヌルの場合の値 Collections.synchronizedMap() によって支えられています HashMap また LinkedHashMap、 一方、使用している場合 TreeMap 、 私たちは持てるヌル値ではなくヌルキー。

HashMapに裏打ちされたCollections.synchronizedMap()nullキーを使用できると主張しましょう。

Map<String, Integer> map = Collections
  .synchronizedMap(new HashMap<String, Integer>());
map.put(null, 1);
Assert.assertTrue(map.get(null).equals(1));

同様に、 Collections.synchronizedMap()ConcurrentHashMapの両方の値でnullサポートを検証できます。

3. パフォーマンスの比較

ConcurrentHashMapCollections.synchronizedMap()。のパフォーマンスを比較してみましょう。この場合、オープンソースフレームワーク Java Microbenchmark Harness ( JMH)からまで、メソッドのパフォーマンスをナノ秒単位で比較します

これらのマップでランダムな読み取りおよび書き込み操作の比較を実行しました。 JMHベンチマークコードを簡単に見てみましょう。

@Benchmark
public void randomReadAndWriteSynchronizedMap() {
    Map<String, Integer> map = Collections.synchronizedMap(new HashMap<String, Integer>());
    performReadAndWriteTest(map);
}

@Benchmark
public void randomReadAndWriteConcurrentHashMap() {
    Map<String, Integer> map = new ConcurrentHashMap<>();
    performReadAndWriteTest(map);
}

private void performReadAndWriteTest(final Map<String, Integer> map) {
    for (int i = 0; i < TEST_NO_ITEMS; i++) {
        Integer randNumber = (int) Math.ceil(Math.random() * TEST_NO_ITEMS);
        map.get(String.valueOf(randNumber));
        map.put(String.valueOf(randNumber), randNumber);
    }
}

1,000アイテムに対して10スレッドで5回の反復を使用して、パフォーマンスベンチマークを実行しました。 ベンチマークの結果を見てみましょう。

Benchmark                                                     Mode  Cnt        Score        Error  Units
MapPerformanceComparison.randomReadAndWriteConcurrentHashMap  avgt  100  3061555.822 ±  84058.268  ns/op
MapPerformanceComparison.randomReadAndWriteSynchronizedMap    avgt  100  3234465.857 ±  60884.889  ns/op
MapPerformanceComparison.randomReadConcurrentHashMap          avgt  100  2728614.243 ± 148477.676  ns/op
MapPerformanceComparison.randomReadSynchronizedMap            avgt  100  3471147.160 ± 174361.431  ns/op
MapPerformanceComparison.randomWriteConcurrentHashMap         avgt  100  3081447.009 ±  69533.465  ns/op
MapPerformanceComparison.randomWriteSynchronizedMap           avgt  100  3385768.422 ± 141412.744  ns/op

上記の結果は、ConcurrentHashMapのパフォーマンスが Collections.synchronizedMap()よりも優れていることを示しています。

4. いつ使用するか

データの一貫性が最も重要な場合はCollections.synchronizedMap()を優先し、読み取り操作よりも書き込み操作がはるかに多いパフォーマンスが重要なアプリケーションにはConcurrentHashMapを選択する必要があります。 。

これは、 Collections.synchronizedMap()では、読み取り/書き込み操作の両方で、各スレッドがオブジェクト全体のロックを取得する必要があるためです。 比較すると、 ConcurrentHashMap を使用すると、スレッドはコレクションの個別のセグメントでロックを取得し、同時に変更を加えることができます。

5. 結論

この記事では、 ConcurrentHashMapCollections.synchronizedMap()の違いを示しました。 また、単純なJMHベンチマークを使用して両方のパフォーマンスを示しました。

いつものように、コードサンプルはGitHubから入手できます。