Java Tips | コレクション:同期ラッパー

Java Java
スポンサーリンク

同期ラッパーは「既存のコレクションを“とりあえず安全にする”ための包み紙」

同期ラッパー(synchronized wrapper)は、Collections.synchronizedListCollections.synchronizedMap のように、既存のコレクションを丸ごと“同期付き”に変換する仕組みです。
内部で synchronized を使ってロックを取り、複数スレッドから同時にアクセスされても壊れないようにしてくれます。

業務では、
「既存コードが ArrayListHashMap を使っているが、マルチスレッド化したい」
「とりあえず安全にしたいが、大規模な書き換えは難しい」
という場面で役に立ちます。


同期ラッパーの基本:Collections.synchronizedXxx

既存のコレクションを“包む”だけでスレッドセーフ化

同期ラッパーは、既存のコレクションをそのまま包むだけで使えます。

import java.util.*;

List<String> raw = new ArrayList<>();
List<String> syncList = Collections.synchronizedList(raw);

Map<String, Integer> rawMap = new HashMap<>();
Map<String, Integer> syncMap = Collections.synchronizedMap(rawMap);
Java

ここでの重要ポイントは、
「元のコレクション(raw)を直接触るとスレッドセーフではない」
という点です。
必ずラップ後の syncList / syncMap を使う必要があります。

同期ラッパーは、addremoveget などの操作を内部で synchronized してくれるため、
複数スレッドが同時に触っても壊れません。


同期ラッパーの落とし穴:イテレーションは自分で同期が必要

for-each するときは外側でロックを取る

同期ラッパーの最大の注意点は、イテレーション(for-each)時に自分で同期を取る必要があることです。

List<String> list = Collections.synchronizedList(new ArrayList<>());

// NG:ConcurrentModificationException の可能性
for (String s : list) {
    System.out.println(s);
}

// OK:外側で同期を取る
synchronized (list) {
    for (String s : list) {
        System.out.println(s);
    }
}
Java

理由は、
「同期ラッパーは“単一操作”だけを同期するが、イテレーションは複数操作の集合だから」
です。

for-each は内部で iterator()hasNext()next() を繰り返すため、
その間に別スレッドが addremove をすると破綻します。

この仕様を知らないと、
「同期ラッパーを使っているのに ConcurrentModificationException が出る」
という事故が起きます。


同期ラッパーと ConcurrentHashMap の違い

“全部ロック”か“細かくロック”か

同期ラッパーは、すべての操作を一つのロックで守る方式です。
そのため、複数スレッドが同時に get しても順番待ちになります。

一方、ConcurrentHashMap は内部を分割していたり、読み取りをロックなしで行えたりするため、
読み取りが多い場面で圧倒的に高速です。

同期ラッパーは「既存コードをとりあえず安全にしたい」場合に向き、
新規開発では ConcurrentHashMap を選ぶことが多いです。


同期ラッパーが役立つ具体例

既存の非同期コードを“最小変更”で安全にしたいとき

例えば、古いコードがこうなっていたとします。

static final List<String> logs = new ArrayList<>();
Java

これをマルチスレッドで使うように変更したいが、
大規模なリファクタリングは難しい場合、同期ラッパーが役立ちます。

static final List<String> logs =
        Collections.synchronizedList(new ArrayList<>());
Java

これだけで、logs.add()logs.remove() が安全になります。


設定値の読み込みなど、更新が少なく読み取りが多い場面

設定値を一度読み込んで、複数スレッドから参照するケース。

Map<String, String> config =
        Collections.synchronizedMap(new HashMap<>());

config.put("mode", "production");
config.put("timeout", "30");
Java

読み取りが多い場合は ConcurrentHashMap の方が高速ですが、
「更新がほぼない」「既存コードを変えたくない」なら同期ラッパーで十分です。


同期ラッパーを使うときに意識したい設計ポイント

どこまで同期を任せるかを決める

同期ラッパーは便利ですが、
「複数操作をまとめて同期したい」 場面では不十分です。

例えば、

if (!list.contains(x)) {
    list.add(x);
}
Java

これは同期ラッパーでも安全ではありません。
containsadd の間に別スレッドが割り込む可能性があるためです。

このような場合は、外側でまとめて同期を取る必要があります。

synchronized (list) {
    if (!list.contains(x)) {
        list.add(x);
    }
}
Java

同期ラッパーは「単一操作の安全性」を提供するだけで、
「複数操作の一貫性」は保証しない、という点を理解しておくことが大切です。


まとめ:同期ラッパーで身につけてほしい感覚

同期ラッパーは、
「既存のコレクションを最小限の変更でスレッドセーフにするための包み紙」
です。

  • 単一操作(add/remove/get)は自動で同期される
  • イテレーションは外側で同期が必要
  • 複数操作をまとめて安全にしたい場合は自分で同期ブロックを書く
  • 新規開発では ConcurrentHashMapCopyOnWriteArrayList の方が向いていることが多い

あなたのコードのどこかに、
「昔からある ArrayListHashMap を複数スレッドで触っていそうな箇所」があれば、
そこを同期ラッパーで包むだけで安全性が大きく改善します。

タイトルとURLをコピーしました