CopyOnWrite は「書き込みのたびに“丸ごとコピー”して安全を買う」仕組み
CopyOnWrite 系コレクション(CopyOnWriteArrayList / CopyOnWriteArraySet)は、
「読み取りが圧倒的に多く、書き込みはたまにしかない」 場面で使う、ちょっと変わったスレッドセーフコレクションです。
名前の通り、
「書き込み(Write)が起きたときに、内部配列をコピー(Copy)してから反映する」
という動きをします。
その代わり、読み取り(for-each や get)はロックなしで高速・安全に行えます。
この“トレードオフ”を理解することが、CopyOnWrite を使いこなす第一歩です。
基本イメージ:読み取りは速く、書き込みは重く
どういうときに向いているのか
CopyOnWrite の設計思想を一言で言うと、
「読み取りの快適さのために、書き込みのコストを犠牲にする」 です。
向いている場面の典型は、こんな感じです。
- リスナー一覧(イベント購読者のリスト)
- 設定値の一覧(めったに変わらない)
- 参照専用のデータ(起動時に読み込んで、あとは読むだけ)
逆に、
「毎秒何百回も add/remove するようなリスト」には向きません。
書き込みのたびに内部配列をコピーするので、すぐに重くなります。
CopyOnWriteArrayList の基本的な使い方
まずは普通の List と同じ感覚で触ってみる
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteBasic {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String s : list) {
System.out.println(s);
}
}
}
Java見た目はほぼ ArrayList と同じです。add / remove / get / for-each など、基本操作はそのまま使えます。
ここで重要なのは、
「for-each 中に別スレッドが add/remove しても、ConcurrentModificationException が出ない」
という点です。
イテレーションは“スナップショット”を見ているので、
ループ開始時点の配列を最後まで安全に走り切れます。
重要ポイント1:イテレーションが“スナップショット”であること
「ループ中に追加された要素は見えない」ことを理解する
CopyOnWrite のイテレーションは、
ループ開始時点の配列をコピーして、それを読み続けるイメージです。
List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
for (String s : list) {
System.out.println(s);
list.add("X"); // ループ中に追加
}
Javaこのコードは例外になりません。
ただし、ループ中に追加した "X" は、そのループでは一切見えません。
出力は A と B だけです。
ここでの重要ポイントは二つです。
一つ目は、「CopyOnWrite の for-each は“開始時点の状態”だけを見る」ということ。
ループ中の変更は反映されません。
二つ目は、「それでいい場面でだけ使うべき」ということ。
「ループ中に追加された要素もすぐ処理したい」なら、CopyOnWrite は向きません。
重要ポイント2:書き込みのたびに配列コピーが走る
add/remove が重くなる理由
CopyOnWriteArrayList の内部は、ざっくり言うと「不変の配列」を持っています。add するときは、
- 現在の配列をコピーして新しい配列を作る
- 新しい配列に要素を追加する
- 参照を新しい配列に差し替える
という流れになります。
要素数が 10 件なら大したことはありませんが、
要素数が 10,000 件のリストに対して頻繁に add すると、
毎回 10,000 件分のコピーが走ることになります。
だからこそ、
「書き込みはたまにしか起きない」
という前提がとても大事です。
具体例1:イベントリスナー一覧に CopyOnWriteArrayList を使う
「リスナーの登録はたまに、イベント発火は頻繁」なケース
イベント駆動のコードでは、
「リスナーの一覧」を複数スレッドから読む・書くことがよくあります。
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class EventBus {
public interface Listener {
void onEvent(String message);
}
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
public void addListener(Listener l) {
listeners.add(l); // たまにしか呼ばれない想定
}
public void removeListener(Listener l) {
listeners.remove(l);
}
public void fire(String message) {
for (Listener l : listeners) { // 頻繁に呼ばれる
l.onEvent(message);
}
}
}
Javaここでの重要ポイントは三つです。
一つ目は、「イベント発火(fire)が頻繁に呼ばれても、for-each がロックなしで安全に回る」こと。
リスナー一覧のスナップショットを見ているので、ConcurrentModificationException も出ません。
二つ目は、「リスナーの追加・削除はたまにしか起きない」という前提で設計していること。
ここが崩れると、CopyOnWrite のメリットよりデメリットが勝ちます。
三つ目は、「リスナー追加・削除とイベント発火が別スレッドでも、コードがシンプルに書ける」こと。
同期ブロックを自分で書かなくてよいのは、実務でかなり嬉しいポイントです。
具体例2:設定値の一覧を CopyOnWriteArrayList で持つ
「起動時に読み込んで、あとはほぼ読むだけ」のデータ
例えば、「許可されたIPアドレスの一覧」をメモリに持っておき、
リクエストごとにチェックするようなケース。
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class AllowedIpList {
private final List<String> ips = new CopyOnWriteArrayList<>();
public void loadFromConfig(List<String> loaded) {
ips.clear();
ips.addAll(loaded);
}
public boolean isAllowed(String ip) {
return ips.contains(ip);
}
}
Javaここでのポイントは二つです。
一つ目は、「isAllowed が高頻度で呼ばれても、ロックなしで安全に動く」こと。contains は内部的に配列を読むだけなので、読み取りが軽いです。
二つ目は、「設定の再読み込み(loadFromConfig)はたまにしか起きない」という前提です。
ここでも「読み取り多・書き込み少」のパターンにハマっています。
CopyOnWrite を選ぶかどうかの判断軸
3つの質問でざっくり決める
CopyOnWrite を使うか迷ったら、次の3つを自分に聞いてみてください。
- 読み取りと書き込み、どちらが圧倒的に多いか
- イテレーション中に変更されても「そのループでは見えなくてよい」か
- 要素数はそこまで大きくならないか(数百〜数千程度までか)
全部「はい」なら、CopyOnWrite はかなり有力な候補です。
一つでも「いいえ」が混ざるなら、ConcurrentHashMap や Collections.synchronizedList など、別の選択肢を検討した方が安全です。
まとめ:CopyOnWrite利用で身につけてほしい感覚
CopyOnWrite 系コレクションは、
単に「スレッドセーフな List/Set」ではなく、
「読み取りの快適さのために、書き込みコストを意図的に高くしている特殊な道具」 です。
読み取りが圧倒的に多く、書き込みはたまにしかない場面を選ぶ。
イテレーションは“スナップショット”であり、ループ中の変更は見えないことを理解する。
書き込みのたびに配列コピーが走るので、頻繁な更新や巨大なリストには使わない。
イベントリスナー一覧や設定値一覧など、「読み取り中心の共有データ」に当てはめてみる。
あなたのコードのどこかに、
「リスナー一覧」「設定値一覧」「参照専用のデータ」を複数スレッドから読んでいる箇所があれば、
そこを一度「CopyOnWrite で書けないか?」という目で眺めてみてください。
その小さな選択が、
「アクセスパターンを意識してコレクションを選べるエンジニア」への、
いい一歩になります。
