同期コレクションと並列コレクション


同期コレクション

同期コレクションは、JDK1.2以降で追加された Collections.synchronizedXxx 系のファクトリメソッドから同期ラッパークラスを作成します。

List<String> list = Collections.synchronizedList(new ArrayList<String>());
Set<String> s = Collections.synchronizedSet(new HashSet<String>());
Map<Long, String> m = Collections.synchronizedMap(new HashMap<Long, String>());

レガシーコレクションである Vector や Hashtable もまた同期コレクションであるが、Collections Framework との親和性に優れる同期ラッパークラスを使用したほうが良いです。


同期コレクションはスレッドセーフですが、イテレーションやプット・イフ・アブセント(存在を確認し、無ければ追加するといった複合アクション)の処理で期待した動きとならないことがあるため注意が必要です。

同期コレクションのイテレーション

例えば以下のコードは同期ラッパーにて正しそうに見えますが、イテレート中に別スレッドからlist操作を行った場合、ConcurrentModificationException が投げられます。

List<String> list = Collections.synchronizedList(new ArrayList<String>());
for (String s : list) {
    doSomething(s);
}


これに対応するには以下のようにイテレート処理を同期するか、イテレート前にリストのクローンを作ってコピーをイテレートする必要があります。

List<String> list = Collections.synchronizedList(new ArrayList<String>());
synchronized(list) {
    for (String s : list) {
        doSomething(s);
    }
}

また注意したいのが、以下のようなリストの文字列連結により toString 内部でイテレート処理が行われるため、同様に ConcurrentModificationException が投げられる可能性があります。

System.out.println("list:" + list);

同期コレクションのプット・イフ・アブセント

以下の例はプット・イフ・アブセント(put-if-absent)の例です。

public static String getLast(List<String> list) {
    int lastIndex = list.size() - 1;
    return list.get(lastIndex);
}
public static void deleteLast(List<String> list) {
    int lastIndex = list.size() - 1;
    list.remove(lastIndex);
}

複数スレッドからの呼び出しタイミングにより IndexOutOfBoundsException が発生する可能性があります。

この場合は以下のように同期ブロックで対象のlistに関する操作をロックする必要があります。

public static String getLast(List<String> list) {
    synchronized (list) {
        int lastIndex = list.size() - 1;
        return list.get(lastIndex);
    }
}
public static void deleteLast(List<String> list) {
    synchronized (list) {
        int lastIndex = list.size() - 1;
        list.remove(lastIndex);
    }
}

並列コレクション

Java5より、同期コレクションを改良した並列コレクションが提供されました。同期コレクションはロックによりコレクションの操作を1スレッドに限定するのに対し、並列コレクションは複数スレッドから同時操作できるように設計されており、スケーラビリティが大きく向上しています。

ConcurrentHashMap

ConcurrentHashMap はロックストライピング(Mapが保持する情報を全てロックするのではなく、ロックの範囲を分割した情報の細かな単位とする)により同期を保ったまま同時アクセスのスループットを向上します。

ConcurrentMap<Long, String> map = new ConcurrentHashMap<Long, String>();

並列コレクションは弱い整合性により、同期コレクションでのイテレート処理で投げられる可能性のあった ConcurrentModificationException が発生することはありません。イテレート処理はイテレータが作られた時の状態で走査されることになります。(miyakawa_takuさんの指摘により削除しました。どもです)


並列コレクションは、同期コレクションのようなマップ全体のロック機能がありません。並列で操作されることが前提となっているためです。そのため、プット・イフ・アブセントのような処理をアトミックに行うためのメソッドが提供されています。

ConcurrentMap<Long, String> map = new ConcurrentHashMap<Long, String>();
・・・
map.putIfAbsent(1L, "value"); // 1L のキーが無ければ追加
map.remove(2L, "value");      // 2L がキーとして有れば削除
map.replace(3L, "value");     // 3L がキーとして有れば置換
map.replace(4L, "old", "new");// 4L がoldとしてマップされていれば置換

同様なクラスにConcurrentLinkedQueueがあります。

CopyOnWriteArrayList

CopyOnWriteArrayList はListの変更があった場合、新たなコピーを作成します。これによりアクセス時の同期が不要になり、変更よりも走査が多用される用途においてスループットが向上します。

List<String> list = new CopyOnWriteArrayList<String>();

イテレート処理はイテレータが作られた時の状態で走査されることになり、別スレッドからの変更の影響は受けません。

CopyOnWriteArraySet

CopyOnWriteArrayList のSet実装です。内部的にCopyOnWriteArrayListを持っており、処理を委譲しています。変更よりも走査が多用される用途においてスループットが向上します。

Set<String> set = new CopyOnWriteArraySet<String>();





Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―