「スレッドセーフList」は“同時に触られても壊れないリスト”
業務システムでは、複数スレッドから同じ List にアクセスする場面が普通に出てきます。
ログを貯める、イベントを溜める、キャッシュ的に使う——どれも「同時アクセス」が起きがちです。
素の ArrayList はスレッドセーフではありません。
複数スレッドが同時に add や remove をすると、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.add や list.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)はロックなしで高速。
書き込み(add、remove)は「コピー+差し替え」で重め。
つまり、
「読み取りが圧倒的に多く、書き込みはたまにしかない」
という場面に向いたスレッドセーフ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 で「設定値などのスナップショット」を安全に共有する。
どれを選ぶかは、「読み取りと書き込みの比率」「要素数」「許容できるコスト」で決める。
あなたのコードのどこかに、ArrayList を static で共有して、複数スレッドから触っていそうな箇所があれば、
そこを一度「どのスレッドセーフListにすべきか?」という目で眺めてみてください。
その小さな見直しが、
「並行処理を前提に、コレクションを設計できるエンジニア」への、
確かな一歩になります。
