Java Tips | コレクション:スレッドセーフList

Java Java
スポンサーリンク

「スレッドセーフList」は“同時に触られても壊れないリスト”

業務システムでは、複数スレッドから同じ List にアクセスする場面が普通に出てきます。
ログを貯める、イベントを溜める、キャッシュ的に使う——どれも「同時アクセス」が起きがちです。

素の ArrayList はスレッドセーフではありません。
複数スレッドが同時に addremove をすると、
ConcurrentModificationException や、最悪「中身が壊れる」ことがあります。

そこで使うのが「スレッドセーフList」です。
今日は代表的な3パターンを、初心者向けにかみ砕いて整理します。


パターン1:Collections.synchronizedList で「丸ごとロック」

一番素直なやり方

既存の List を「とりあえずスレッドセーフにしたい」なら、
Collections.synchronizedList が一番シンプルです。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SyncListExample {

    public static void main(String[] args) {
        List<String> raw = new ArrayList<>();
        List<String> list = Collections.synchronizedList(raw);

        list.add("A");
        list.add("B");
    }
}
Java

ここでの重要ポイントは二つです。

一つ目は、「list.addlist.remove が内部で同期(ロック)される」ことです。
複数スレッドが同時に呼んでも、1つずつ順番に処理されるので、リストが壊れません。

二つ目は、「イテレーション(for-each)には注意が必要」という点です。
公式に「イテレーションするときは、外側で同期を取れ」と言われています。

synchronized (list) {
    for (String s : list) {
        System.out.println(s);
    }
}
Java

これを忘れると、
「片方のスレッドがループ中に、別のスレッドが add/remove して ConcurrentModificationException
という事故が起きます。

「とりあえず安全にしたい」「アクセス頻度はそこまで高くない」なら、
synchronizedList は分かりやすくて良い選択です。


パターン2:CopyOnWriteArrayList で「読み取りが圧倒的に多いとき」

仕組みのイメージ

CopyOnWriteArrayList は、
「書き込みのたびに内部配列をコピーする」という、ちょっと変わった List です。

読み取り(get、for-each)はロックなしで高速。
書き込み(addremove)は「コピー+差し替え」で重め。

つまり、
「読み取りが圧倒的に多く、書き込みはたまにしかない」
という場面に向いたスレッドセーフListです。

具体例:イベントリスナーの登録

典型的な使いどころは、「イベントリスナーの一覧」です。

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class EventBus {

    private final List<Listener> listeners = new CopyOnWriteArrayList<>();

    public void addListener(Listener l) {
        listeners.add(l); // たまにしか呼ばれない
    }

    public void fireEvent(Event e) {
        for (Listener l : listeners) { // 読み取りは頻繁
            l.onEvent(e);
        }
    }
}
Java

ここでの重要ポイントは三つです。

一つ目は、「for-each 中に別スレッドが add/remove しても安全」ということです。
イテレーションは“スナップショット”を見ているので、ConcurrentModificationException が起きません。

二つ目は、「書き込みが多いとパフォーマンスが悪化する」ことです。
要素数が多い状態で頻繁に add すると、毎回配列コピーが走って重くなります。

三つ目は、「読み取りが圧倒的に多い場面にだけ使う」という割り切りが大事、ということです。
設定値の一覧、リスナー一覧、参照専用のデータなどが向いています。


パターン3:不変Listを共有し、更新時は“丸ごと差し替え”

「読み取り専用のスナップショット」を配る設計

もう一つよく使われるのが、
「List 自体は不変(変更不可)にしておき、更新が必要なときは“新しい List を丸ごと作って差し替える”」
という設計です。

例えば、設定値の一覧を保持するクラス。

import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

public class ConfigHolder {

    private final AtomicReference<List<String>> ref =
            new AtomicReference<>(Collections.emptyList());

    public List<String> getValues() {
        return ref.get(); // 読み取りはロックなし
    }

    public void updateValues(List<String> newValues) {
        ref.set(Collections.unmodifiableList(newValues));
    }
}
Java

ここでの重要ポイントは二つです。

一つ目は、「getValues で返される List は不変(unmodifiableList)なので、呼び出し側が変更できない」ことです。
これにより、「誰かが勝手に書き換えて壊す」ことを防げます。

二つ目は、「更新は“新しい List を作って AtomicReference にセットする”だけ」ということです。
読み取り側は常に「どこかの時点のスナップショット」を見ているので、同期なしで安全に読めます。

このパターンは、
「読み取りが多く、更新はたまに」「更新時に一瞬だけ古い値が見えても構わない」
という設定系のデータに向いています。


どのスレッドセーフListを選ぶかの目安

読み取りと書き込みのバランスで考える

ざっくりとした目安はこうです。

読み取りも書き込みもそこそこ
Collections.synchronizedList(まずはこれで十分なことが多い)

読み取りが圧倒的に多く、書き込みはごくたまに
CopyOnWriteArrayList または「不変List+AtomicReference」

書き込みも多く、要素数も多い
→ そもそも List でいいのか? キューやマップ、専用ライブラリを検討する

ここで一番大事なのは、
「とりあえず全部スレッドセーフにする」のではなく、「どうアクセスされるか」をイメージして選ぶ
という姿勢です。


まとめ:スレッドセーフListで身につけてほしい感覚

スレッドセーフListは、
単に「NPE や例外を避けるための保険」ではなく、
「複数スレッドからのアクセスパターンを意識して、適切な“箱”を選ぶ技術」です。

Collections.synchronizedList で「とりあえず壊れないリスト」を作る。
CopyOnWriteArrayList で「読み取りが圧倒的に多いリスト」を気持ちよく扱う。
不変List+AtomicReference で「設定値などのスナップショット」を安全に共有する。
どれを選ぶかは、「読み取りと書き込みの比率」「要素数」「許容できるコスト」で決める。

あなたのコードのどこかに、
ArrayListstatic で共有して、複数スレッドから触っていそうな箇所があれば、
そこを一度「どのスレッドセーフListにすべきか?」という目で眺めてみてください。

その小さな見直しが、
「並行処理を前提に、コレクションを設計できるエンジニア」への、
確かな一歩になります。

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