Java 逆引き集 | Collections.synchronizedList 等(同期ラッパー) — 古い同期方法

Java Java
スポンサーリンク

Collections.synchronizedList 等(同期ラッパー) — 古い同期方法

読み書きが同時に起きる環境で、既存の可変コレクションを「ひとまず安全に」使いたいときに使うのが同期ラッパー。Collections.synchronizedList / synchronizedSet / synchronizedMap は、全操作を1つのロックで守る古典的な手法です。仕組み、使い方、落とし穴、現代的な代替まで初心者向けに整理します。


同期ラッパーの基本

  • 目的: 既存の非スレッドセーフなコレクションを「排他制御付きビュー」に包み、同時アクセスでも壊れないようにする。
  • 対象: List, Set, Map などほぼ全てのコレクションに用意あり。
  • 方式: 1つのロック(モニタ)で「全操作」を直列化するため、読み書き競合が多いと性能が落ちる。
  • イテレーションの注意: 反復処理(for-each, iterator)は「外側からロック」を取ってから行う必要がある。

使い方とコード例

List を同期化する

import java.util.*;

public class SyncListDemo {
    public static void main(String[] args) {
        List<String> raw = new ArrayList<>();
        raw.add("A"); raw.add("B"); raw.add("C");

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

        // 単発操作(add/get/remove)は安全
        syncList.add("D");
        System.out.println(syncList.get(0)); // A

        // 反復処理は外側で同期化(必須)
        synchronized (syncList) {
            for (String s : syncList) {
                System.out.println(s);
            }
        }
    }
}
Java

Set / Map も同様

Set<Integer> syncSet = Collections.synchronizedSet(new HashSet<>());
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());

// 単発操作は安全
syncSet.add(1);
syncMap.put("A", 10);

// 反復処理(例:Mapのエントリ走査)は外側同期
synchronized (syncMap) {
    for (var e : syncMap.entrySet()) {
        System.out.println(e.getKey() + "=" + e.getValue());
    }
}
Java

反復処理の正しい書き方

  • 理由: 同期ラッパーの iterator() 取得後に他スレッドが構造を変更すると、走査中に不整合や例外が起き得るため。
  • パターン:
synchronized (syncList) {
    Iterator<String> it = syncList.iterator();
    while (it.hasNext()) {
        String v = it.next();
        // …処理…
    }
}
Java

よくある落とし穴と回避策

  • 落とし穴: 反復処理で同期しない
    • 症状: 走査中の変更で例外や取りこぼし。
    • 回避: 走査は常に synchronized(コレクション) で囲む。
  • 落とし穴: ロックの粒度が粗くて遅い
    • 症状: 読み取り多数でも直列化され、スループット低下。
    • 回避: 読み取り中心ならスナップショット(new ArrayList<>(list))を配布、更新中心なら並行コレクションへ移行。
  • 落とし穴: デッドロックやロック順序の問題
    • 症状: 複数コレクションを入れ替えでロック → 順序逆転で停止。
    • 回避: ロック順序を統一、可能なら1コレクションに集約。
  • 落とし穴: マップの複合操作を分割
    • 症状: if (!map.containsKey(k)) map.put(k,v) のような分割操作で競合。
    • 回避: 同期ブロックでまとめる、または ConcurrentHashMap の原子メソッドを使う(putIfAbsent, computeIfAbsent)。

いつ使うか(適用シナリオ)

  • 短期的な安全化: 既存コードをそのまま使いたい、急ぎで「壊れない」ようにしたい。
  • 小規模・更新頻度が低い: 走査も少なく、ロック競合が軽い場面。
  • 単純な排他: 複雑な同時更新ロジックが不要な場面。

現代的な代替(推奨)

  • ConcurrentHashMap: マップ共有は原子操作が豊富で、並行性が高い。
  • CopyOnWriteArrayList/Set: 読み取りが圧倒的に多く、更新が稀な場面に強い(走査はロック不要、更新はコピーコスト)。
  • ConcurrentLinkedQueue/Deque: キューや双方向キューの並行性が必要な場面。
  • 不変コレクションの配布: 公開用は List.copyOfList.of で不変にして、更新側は内部の可変構造で管理。

テンプレート集

  • 同期ラッパーの生成
List<T> syncList = Collections.synchronizedList(new ArrayList<>());
Set<T> syncSet   = Collections.synchronizedSet(new HashSet<>());
Map<K,V> syncMap = Collections.synchronizedMap(new HashMap<>());
Java
  • 反復処理(必ず外側同期)
synchronized (syncList) {
    for (T t : syncList) { /* … */ }
}
Java
  • スナップショットで読み取り配布
List<T> snapshot;
synchronized (syncList) {
    snapshot = new ArrayList<>(syncList);
}
// スナップショットはロック不要で安全に走査できる
for (T t : snapshot) { /* … */ }
Java
  • 複合操作は同期ブロックで一括
synchronized (syncMap) {
    if (!syncMap.containsKey(k)) {
        syncMap.put(k, v);
    }
}
Java

例題で理解する

例題1: ログの安全な集約と配布

class SafeLogs {
    private final List<String> logs = Collections.synchronizedList(new ArrayList<>());

    public void record(String line) {
        logs.add(line); // 単発操作は安全
    }

    public List<String> snapshot() {
        synchronized (logs) {
            return new ArrayList<>(logs); // スナップショットを返す
        }
    }
}
Java
  • 走査はスナップショットでロック不要。呼び出し側は安全に閲覧できる。

例題2: 同期 Map の反復と更新

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

// 更新(単発)
counts.merge("apple", 1, Integer::sum);

// 走査(必ず同期)
synchronized (counts) {
    for (var e : counts.entrySet()) {
        System.out.println(e.getKey() + "=" + e.getValue());
    }
}
Java
  • 複合更新は同期ブロックでまとめる。高頻度の集計なら ConcurrentHashMap がより良い。

まとめ

  • Collections.synchronizedXxx は「古典的な全体ロック」による安全化。反復処理は外側同期が必須で、読み取り中心でも直列化されるため性能は伸びにくい。
  • 速さや細かな原子性が必要なら、用途に応じて並行コレクション(ConcurrentHashMap など)や不変コレクションを選ぶ。短期的な安全化や小規模用途では、同期ラッパーは簡単で有効。
タイトルとURLをコピーしました