JavaにおけるConcurrentHashMap
JavaのConcurrentHashMapクラスは、並行コレクションクラスの一部です。これはハッシュテーブルの実装であり、並行的な取得と更新をサポートしています。これはマルチスレッド環境でConcurrentModificationExceptionを回避するために使用されます。
ConcurrentHashMapは日本語で次のように言い換えられます:
「並列ハッシュマップ」
コレクションをイテレーション中に変更しようとすると、ConcurrentModificationExceptionが発生します。Java 1.5では、このシナリオを克服するためにjava.util.concurrentパッケージにConcurrentクラスが導入されました。ConcurrentHashMapは、マップのイテレーション中でもマップを変更することができるマップの実装です。ConcurrentHashMapの操作はスレッドセーフです。ConcurrentHashMapでは、キーと値のどちらにもnullを許可しません。
JavaのConcurrentHashMapの例
ConcurrentHashMapクラスは、HashMapに似ていますが、スレッドセーフであり、反復中に修正を許可します。
package com.scdev.util;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
//ConcurrentHashMap
Map<String,String> myMap = new ConcurrentHashMap<String,String>();
myMap.put("1", "1");
myMap.put("2", "1");
myMap.put("3", "1");
myMap.put("4", "1");
myMap.put("5", "1");
myMap.put("6", "1");
System.out.println("ConcurrentHashMap before iterator: "+myMap);
Iterator<String> it = myMap.keySet().iterator();
while(it.hasNext()){
String key = it.next();
if(key.equals("3")) myMap.put(key+"new", "new3");
}
System.out.println("ConcurrentHashMap after iterator: "+myMap);
//HashMap
myMap = new HashMap<String,String>();
myMap.put("1", "1");
myMap.put("2", "1");
myMap.put("3", "1");
myMap.put("4", "1");
myMap.put("5", "1");
myMap.put("6", "1");
System.out.println("HashMap before iterator: "+myMap);
Iterator<String> it1 = myMap.keySet().iterator();
while(it1.hasNext()){
String key = it1.next();
if(key.equals("3")) myMap.put(key+"new", "new3");
}
System.out.println("HashMap after iterator: "+myMap);
}
}
出力:
ConcurrentHashMap before iterator: {1=1, 5=1, 6=1, 3=1, 4=1, 2=1}
ConcurrentHashMap after iterator: {1=1, 3new=new3, 5=1, 6=1, 3=1, 4=1, 2=1}
HashMap before iterator: {3=1, 2=1, 1=1, 6=1, 5=1, 4=1}
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextEntry(HashMap.java:793)
at java.util.HashMap$KeyIterator.next(HashMap.java:828)
at com.test.ConcurrentHashMapExample.main(ConcurrentHashMapExample.java:44)
出力から明らかなのは、ConcurrentHashMapは反復処理中にマップの新しいエントリを処理する一方、HashMapはConcurrentModificationExceptionをスローすることです。例外のスタックトレースを詳しく見てみましょう。次の文が例外をスローしました。
String key = it1.next();
新しいエントリがHashMapに挿入されましたが、イテレータが失敗しています。実際には、コレクションオブジェクト上のイテレータは fail-fast です。つまり、コレクションオブジェクトの構造やエントリの数に対する変更があると、例外が発生します。
イテレーターは、コレクションの変更についてどのように知るのですか?
私たちはHashMapからキーのセットを取り、それを繰り返し処理しています。HashMapには変更回数をカウントする変数があり、イテレータは次のエントリを取得するためにnext()関数を呼び出す際にそれを利用します。HashMap.java:
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient volatile int modCount;
新しいエントリを挿入する際にイテレータループから抜けるために、コードを少し変更しましょう。put呼び出しの後に、単純にブレーク文を追加するだけで済みます。
if(key.equals("3")){
myMap.put(key+"new", "new3");
break;
}
上記のコードの出力結果:
ConcurrentHashMap before iterator: {1=1, 5=1, 6=1, 3=1, 4=1, 2=1}
ConcurrentHashMap after iterator: {1=1, 3new=new3, 5=1, 6=1, 3=1, 4=1, 2=1}
HashMap before iterator: {3=1, 2=1, 1=1, 6=1, 5=1, 4=1}
HashMap after iterator: {3=1, 2=1, 1=1, 3new=new3, 6=1, 5=1, 4=1}
キー値が変更された場合、何が起こりますか? (Kī kachi ga henkō sareta baai, nani ga okorimasu ka?)
新しいエントリを追加せず、既存のキーと値のペアを更新した場合、例外が発生するでしょうか?元のプログラムのコードを変更して、確認してみましょう。
//myMap.put(key+"new", "new3");
myMap.put(key, "new3");
構造は同じままですが、コレクションが修正されたため、例外はありません。
さらに読む
コレクションオブジェクトとイテレータを作成する際に、その角括弧に気付きましたか?それがジェネリクスと呼ばれるものです。ジェネリクスは、ランタイムでのClassCastExceptionを避けるために、コンパイル時の型チェックに非常に強力です。Javaのジェネリクスの詳細については、Java Genericsの例を参照してください。また、Javaのコレクションインタビューの質問とイテレータデザインパターンも読んでください。
私たちのGitHubリポジトリから、さらに多くのJavaコレクションの例をチェックアウトすることができます。
参照:APIドキュメント