📚 この回で学ぶこと
学習時間: 約45分
難易度: ★★★★☆(4/5)
前提知識: HashMap/ArrayList の理解
学習目標
- マルチスレッドの基礎を理解する
- Race Condition(競合状態) の危険性を知る
- スレッドセーフなコレクション の使い方をマスターする
- 実務での使い分けができるようになる
マルチスレッド、スレッドセーフは聞きなれない用語かもしれませんが
実務では考慮しないとシステムが壊れる場合があります
検証結果を視覚で得るため、絵文字多めでまとめてあります
なぜスレッドセーフが必要なのか?
問題のシナリオ:オンラインRPGのアイテム管理
// 複数のプレイヤーが同時にアイテムを取得するシステム
public class ItemManager {
private Map<String, Integer> inventory = new HashMap<>();
public void addItem(String itemName, int count) {
int current = inventory.getOrDefault(itemName, 0);
// ⚠️ ここで他のスレッドが同時にアクセスすると...
inventory.put(itemName, current + count);
}
}
問題点:
- プレイヤーA と プレイヤーB が同時に「ポーション」を取得
- どちらも現在数: 10 を読み取る
- A が 1個追加 → 11個
- B が 1個追加 → 11個(本来は12個のはず!)
- データの不整合が発生
目次
1. マルチスレッドの基礎
1.1 スレッドとは?
// スレッドの基本
public class SimpleThreadExample {
public static void main(String[] args) {
// メインスレッド
System.out.println("メインスレッド開始");
// 新しいスレッドを作成
Thread worker = new Thread(() -> {
System.out.println("ワーカースレッド実行中");
});
worker.start();
System.out.println("メインスレッド終了");
}
}
実行結果:
メインスレッド開始
メインスレッド終了
ワーカースレッド実行中 ← 順序が保証されない!
1.2 Race Condition(競合状態)とは?
問題のシナリオ: 2つのスレッドが同時に同じデータにアクセスすると、データの不整合が発生します。上の図は、タイムライン形式で2人のプレイヤーが同時にアイテムを取得した際に、1個が失われる様子を視覚化しています。
public class RaceConditionDemo {
private int counter = 0;
public void increment() {
// この3つの操作は「アトミック」ではない
int temp = counter; // 1. 読み取り
temp = temp + 1; // 2. 計算
counter = temp; // 3. 書き込み
}
public static void main(String[] args) throws InterruptedException {
RaceConditionDemo demo = new RaceConditionDemo();
// 2つのスレッドが同時に10000回ずつインクリメント
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
demo.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
demo.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("期待値: 20000");
System.out.println("実際の値: " + demo.counter); // 15000くらい?
}
}
実行結果
期待値: 20000
実際の値: 15133
なぜ20000にならない?
- スレッドAとBが同時に
counterを読み取る - 両方が同じ値(例: 100)を読む
- 両方が 101 を書き込む
- 1回分のインクリメントが失われる
2. スレッドセーフなコレクションの種類
2.1 ConcurrentHashMap
特徴:
- HashMap のスレッドセーフ版
- 複数スレッドが同時に読み書き可能
- ロックを細かく分割(セグメント)して高速化
上図は、HashMap(左・赤背景・混乱)と ConcurrentHashMap(右・緑背景・整然)の比較です。下部の性能比較グラフと推奨表で、実務での選択基準が一目でわかります。
実践例:オンラインRPGのアイテム管理
デモ実行
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
public class ThreadSafeDemo {
// テスト設定
private static final int THREAD_COUNT = 10; // スレッド数
private static final int OPERATIONS_PER_THREAD = 1000; // 各スレッドの操作回数
private static final int EXPECTED_TOTAL = THREAD_COUNT * OPERATIONS_PER_THREAD;
/**
* HashMap(非スレッドセーフ)でテスト
*/
public static void testHashMap() throws InterruptedException {
Map<String, Integer> unsafeMap = new HashMap<>();
unsafeMap.put("ポーション", 0);
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(THREAD_COUNT);
// 複数スレッドを同時に起動
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
try {
startLatch.await(); // 全スレッドが準備完了するまで待機
for (int j = 0; j < OPERATIONS_PER_THREAD; j++) {
// ⚠️ この操作はアトミックではない
Integer current = unsafeMap.get("ポーション"); // 1. 読み取り
unsafeMap.put("ポーション", current + 1); // 2. 書き込み
// ⚠️ 1と2の間に他スレッドが割り込む可能性
}
} catch (Exception e) {
e.printStackTrace();
} finally {
endLatch.countDown();
}
}).start();
}
// 全スレッドを同時にスタート
startLatch.countDown();
// 全スレッドの完了を待つ
endLatch.await();
int result = unsafeMap.get("ポーション");
System.out.println("❌ HashMap(非スレッドセーフ):");
System.out.println(" 期待値: " + EXPECTED_TOTAL);
System.out.println(" 実際の値: " + result);
System.out.println(" 差分: " + (EXPECTED_TOTAL - result) + " 個が失われた ");
}
/**
* ConcurrentHashMap(スレッドセーフ)でテスト
*/
public static void testConcurrentHashMap() throws InterruptedException {
ConcurrentHashMap<String, Integer> safeMap = new ConcurrentHashMap<>();
safeMap.put("ポーション", 0);
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(THREAD_COUNT);
// 複数スレッドを同時に起動
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
try {
startLatch.await(); // 全スレッドが準備完了するまで待機
for (int j = 0; j < OPERATIONS_PER_THREAD; j++) {
// ✅ merge()はアトミックな操作
safeMap.merge("ポーション", 1, Integer::sum);
// ✅ 読み取り→計算→書き込みが1つの操作として実行
}
} catch (Exception e) {
e.printStackTrace();
} finally {
endLatch.countDown();
}
}).start();
}
// 全スレッドを同時にスタート
startLatch.countDown();
// 全スレッドの完了を待つ
endLatch.await();
int result = safeMap.get("ポーション");
System.out.println("\n✅ ConcurrentHashMap(スレッドセーフ):");
System.out.println(" 期待値: " + EXPECTED_TOTAL);
System.out.println(" 実際の値: " + result);
if (result == EXPECTED_TOTAL) {
System.out.println(" 結果: 完璧!データ破損なし");
} else {
System.out.println(" 差分: " + (EXPECTED_TOTAL - result) + " 個が失われた");
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=".repeat(60));
System.out.println("ConcurrentHashMap スレッドセーフ性能テスト");
System.out.println("=".repeat(60));
System.out.println("設定:");
System.out.println(" - スレッド数: " + THREAD_COUNT);
System.out.println(" - 各スレッドの操作回数: " + OPERATIONS_PER_THREAD);
System.out.println(" - 期待合計: " + EXPECTED_TOTAL);
System.out.println("=".repeat(60));
// テスト1: HashMap(失敗する)
System.out.println("\n【テスト1】HashMap(非スレッドセーフ)");
System.out.println("-".repeat(60));
testHashMap();
// テスト2: ConcurrentHashMap(成功する)
System.out.println("\n【テスト2】ConcurrentHashMap(スレッドセーフ)");
System.out.println("-".repeat(60));
testConcurrentHashMap();
System.out.println("\n" + "=".repeat(60));
System.out.println("📊 結論:");
System.out.println(" HashMap: Race Conditionによりデータ破損が発生");
System.out.println(" ConcurrentHashMap: スレッドセーフで正確");
System.out.println("=".repeat(60));
}
}
実行結果
============================================================
ConcurrentHashMap スレッドセーフ性能テスト
============================================================
設定:
- スレッド数: 10
- 各スレッドの操作回数: 1000
- 期待合計: 10000
============================================================
【テスト1】HashMap(非スレッドセーフ)
------------------------------------------------------------
❌ HashMap(非スレッドセーフ):
期待値: 10000
実際の値: 1633
差分: 8367 個が失われた
【テスト2】ConcurrentHashMap(スレッドセーフ)
------------------------------------------------------------
✅ ConcurrentHashMap(スレッドセーフ):
期待値: 10000
実際の値: 10000
結果: 完璧!データ破損なし
============================================================
📊 結論:
HashMap: Race Conditionによりデータ破損が発生
ConcurrentHashMap: スレッドセーフで正確
============================================================
重要なポイント
- HashMap の結果は実行ごとに変わる(不確定)
- ConcurrentHashMap は常に正確(確定的)
- これがスレッドセーフの価値!
2.2 CopyOnWriteArrayList
特徴:
- ArrayList のスレッドセーフ版
- 読み取りが多く、書き込みが少ない場合に最適
- 書き込み時に内部配列をコピー(Copy-On-Write)
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListDemo {
public static void main(String[] args) {
// ❌ 非スレッドセーフ
List<String> unsafeList = new ArrayList<>();
// ✅ スレッドセーフ(読み取り多用向け)
List<String> safeList = new CopyOnWriteArrayList<>();
safeList.add("勇者");
safeList.add("戦士");
safeList.add("魔法使い");
// イテレーション中に他のスレッドが変更しても安全
for (String member : safeList) {
System.out.println(member);
}
}
}
実践例:パーティメンバーのリアルタイム管理:
public class ThreadSafePartyManager {
private final CopyOnWriteArrayList<String> members
= new CopyOnWriteArrayList<>();
public void addMember(String name) {
members.add(name);
System.out.println("✅ " + name + " がパーティに参加");
}
public void removeMember(String name) {
if (members.remove(name)) {
System.out.println("✅ " + name + " がパーティから離脱");
}
}
public void showMembers() {
System.out.println("\n👥 現在のパーティメンバー:");
members.forEach(member -> System.out.println(" - " + member));
}
public int getMemberCount() {
return members.size();
}
}
実行デモ
- テスト1:並行アクセス(参加のみ)
- テスト2:ArrayList との比較(失敗するケース)
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
/**
* CopyOnWriteArrayList のスレッドセーフ性を実証するデモ
*
* シナリオ:オンラインRPGのパーティ管理システム
* - 複数のプレイヤーが同時にパーティに参加/離脱
* - パーティメンバーの表示(イテレーション)も同時実行
*/
public class PartyManagerDemo {
private static final int PLAYER_COUNT = 10; // 同時接続プレイヤー数
/**
* テスト1:並行アクセス(参加のみ)
*/
public static void testConcurrentAdd() throws InterruptedException {
System.out.println("【テスト1】複数プレイヤーが同時に参加");
System.out.println("-".repeat(60));
ThreadSafePartyManager manager = new ThreadSafePartyManager();
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(PLAYER_COUNT);
// 10人のプレイヤーが同時に参加を試みる
for (int i = 1; i <= PLAYER_COUNT; i++) {
final int playerNum = i;
new Thread(() -> {
try {
startLatch.await(); // 全員が準備完了するまで待機
String role = (playerNum % 3 == 0) ? "魔法使い" :
(playerNum % 3 == 1) ? "戦士" : "僧侶";
manager.addMember(role + playerNum);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
endLatch.countDown();
}
}, "Player-" + i).start();
}
// 全プレイヤーを同時にスタート
startLatch.countDown();
// 全プレイヤーの完了を待つ
endLatch.await();
manager.showMembers();
System.out.println("✅ 期待: 10人、実際: " + manager.getMemberCount() + "人");
System.out.println();
}
/**
* テスト2:ArrayList との比較(失敗するケース)
*/
public static void testArrayListFailure() throws InterruptedException {
System.out.println("【テスト2】ArrayList(非スレッドセーフ)との比較");
System.out.println("-".repeat(60));
ArrayList<String> unsafeList = new ArrayList<>();
// 初期データ
for (int i = 1; i <= 5; i++) {
unsafeList.add("メンバー" + i);
}
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(10);
// 5スレッドが追加
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(() -> {
try {
startLatch.await();
unsafeList.add("新規" + num);
} catch (Exception e) {
System.out.println("❌ 例外発生: " + e.getClass().getSimpleName());
} finally {
endLatch.countDown();
}
}).start();
}
// 5スレッドがイテレーション
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
try {
startLatch.await();
// ⚠️ イテレーション中に他スレッドが変更すると...
for (String member : unsafeList) {
System.out.print(member + " ");
}
System.out.println();
} catch (Exception e) {
System.out.println("❌ 例外発生: " + e.getClass().getSimpleName());
} finally {
endLatch.countDown();
}
}).start();
}
startLatch.countDown();
endLatch.await();
System.out.println("\n⚠️ ArrayList は ConcurrentModificationException が発生する可能性");
System.out.println("✅ CopyOnWriteArrayList なら安全!");
System.out.println();
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=".repeat(60));
System.out.println("CopyOnWriteArrayList パーティ管理デモ");
System.out.println("=".repeat(60));
System.out.println();
// テスト1: 並行追加
testConcurrentAdd();
// テスト2: ArrayList との比較
testArrayListFailure();
System.out.println("=".repeat(60));
System.out.println("結論:");
System.out.println(" ✅ CopyOnWriteArrayList は完全にスレッドセーフ");
System.out.println(" ✅ 読み取り(イテレーション)が高速");
System.out.println(" ✅ ConcurrentModificationException が発生しない");
System.out.println(" ⚠️ 書き込みが多い場合はコストが高い");
System.out.println("=".repeat(60));
}
}
実行結果
============================================================
CopyOnWriteArrayList パーティ管理デモ
============================================================
【テスト1】複数プレイヤーが同時に参加
------------------------------------------------------------
✅ 僧侶2 がパーティに参加
✅ 魔法使い9 がパーティに参加
✅ 戦士7 がパーティに参加
✅ 戦士1 がパーティに参加
✅ 僧侶8 がパーティに参加
✅ 魔法使い6 がパーティに参加
✅ 魔法使い3 がパーティに参加
✅ 戦士4 がパーティに参加
✅ 僧侶5 がパーティに参加
✅ 戦士10 がパーティに参加
👥 現在のパーティメンバー:
- 僧侶8
- 魔法使い9
- 戦士7
- 戦士10
- 僧侶2
- 戦士1
- 魔法使い6
- 魔法使い3
- 戦士4
- 僧侶5
✅ 期待: 10人、実際: 10人
【テスト2】ArrayList(非スレッドセーフ)との比較
------------------------------------------------------------
❌ 例外発生: ConcurrentModificationException
❌ 例外発生: ConcurrentModificationException
メンバー1 メンバー1 メンバー2 メンバー1 メンバー2 メンバー3 メンバー4 メンバー5 新規2 新規3 新規5
❌ 例外発生: ConcurrentModificationException
メンバー3 メンバー4 メンバー5 新規2 新規3 新規5
⚠️ ArrayList は ConcurrentModificationException が発生する可能性
✅ CopyOnWriteArrayList なら安全!
============================================================
結論:
✅ CopyOnWriteArrayList は完全にスレッドセーフ
✅ 読み取り(イテレーション)が高速
✅ ConcurrentModificationException が発生しない
⚠️ 書き込みが多い場合はコストが高い
============================================================
2.3 Collections.synchronizedMap() / synchronizedList()
特徴:
- 既存のコレクションをスレッドセーフにラップ
- すべての操作をロック(遅い)
- ConcurrentHashMap より性能が劣る
import java.util.*;
public class SynchronizedCollectionDemo {
public static void main(String[] args) {
// HashMap をスレッドセーフにラップ
Map<String, Integer> syncMap =
Collections.synchronizedMap(new HashMap<>());
// ArrayList をスレッドセーフにラップ
List<String> syncList =
Collections.synchronizedList(new ArrayList<>());
syncMap.put("HP", 100);
syncList.add("ポーション");
// ⚠️ イテレーション時は手動で同期が必要
synchronized (syncList) {
for (String item : syncList) {
System.out.println(item);
}
}
}
}
3. 使い分けガイド
3.1 フローチャート
上図はゲームのスキルツリー風デザインで、実務でのコレクション選択をサポートします。左パネルに性能比較、右パネルに実務例、下部にクイックリファレンス表を配置しています。
テキスト版フローチャート(参考):
マルチスレッドで使う?
├─ NO → HashMap / ArrayList(通常版)
└─ YES
├─ Map系?
│ ├─ 読み書きが混在 → ConcurrentHashMap ⭐推奨
│ └─ 読み書きが少ない → Collections.synchronizedMap
│
└─ List系?
├─ 読み取りがほとんど → CopyOnWriteArrayList ⭐推奨
├─ 書き込みが多い → Collections.synchronizedList
└─ キュー操作が必要 → BlockingQueue
3.2 使い分け早見表
| コレクション | スレッド セーフ |
性能 | 用途 |
|---|---|---|---|
| HashMap | ❌ | ⚡⚡⚡ | シングルスレッド |
| ConcurrentHashMap | ✅ | ⚡⚡ | マルチスレッド(読み書き混在) |
| SynchronizedMap | ✅ | ⚡ | マルチスレッド(読み書き少) |
| ArrayList | ❌ | ⚡⚡⚡ | シングルスレッド |
| CopyOnWriteArrayList | ✅ | ⚡⚡ | マルチスレッド(読み取り多) |
| SynchronizedList | ✅ | ⚡ | マルチスレッド(書き込み多) |
3.3 Listの使い分けの指針
| シナリオ | 推奨 |
|---|---|
| 読み取り多、書き込み少 | ✅ CopyOnWriteArrayList |
| 書き込み多、読み取り少 | ❌ Collections.synchronizedList |
| イテレーション中に変更 | ✅ CopyOnWriteArrayList |
| リアルタイム性が必要 | ❌ CopyOnWriteArrayList(スナップショット) |
例:
✅ 設定値のリスト(読み取り多)
✅ リスナーのリスト(読み取り多)
✅ パーティメンバー(読み取り多、表示頻度高)
❌ チャットログ(書き込み多)
❌ リアルタイムランキング(即座の反映が必要)
3.4 スレッドセーフの代償
-
利点
✅ 完全にスレッドセーフ
✅ イテレーションが高速
✅ 例外が発生しない -
欠点
⚠️ 書き込みが遅い(配列全体をコピー)
⚠️ メモリ使用量が増加
⚠️ 最新の変更がすぐに反映されない(スナップショット)
4. よくある間違いと対策
❌ 間違い1: synchronizedブロックの外でイテレーション
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());
// ❌ ダメな例
for (String key : map.keySet()) { // ConcurrentModificationException
System.out.println(key);
}
// ✅ 正しい例
synchronized (map) {
for (String key : map.keySet()) {
System.out.println(key);
}
}
❌ 間違い2: putIfAbsent の誤用
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// ❌ ダメな例(アトミックでない)
if (!map.containsKey("HP")) {
map.put("HP", 100);
}
// ✅ 正しい例(アトミック)
map.putIfAbsent("HP", 100);
❌ 間違い3: CopyOnWriteArrayList の誤用
// ❌ 書き込みが多い場合は遅い
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 100000; i++) {
list.add("item" + i); // 毎回配列をコピー 💥
}
// ✅ 書き込みが多い場合は別の方法
List<String> list = Collections.synchronizedList(new ArrayList<>());
練習問題
問題1: どのコレクションを選ぶ?
以下のシナリオで最適なコレクションを選んでください:
1.オンラインゲームのチャットログ(読み取りがほとんど)
- ArrayList
- CopyOnWriteArrayList
- Collections.synchronizedList
2.リアルタイムのアイテム取引システム(読み書き頻繁)
- HashMap
- ConcurrentHashMap
- Collections.synchronizedMap
-
タスクキュー(追加と取得を繰り返す)
- ArrayList
- LinkedList
解答を見る
1.CopyOnWriteArrayList ✅(読み取りが多い)
2.ConcurrentHashMap ✅(読み書き頻繁、高性能)
まとめ
重要ポイント
1.HashMap は非スレッドセーフ
- マルチスレッドでは
ConcurrentHashMapを使う
2.ConcurrentHashMap vs SynchronizedMap
- 性能:
ConcurrentHashMap≫SynchronizedMap - 基本的に
ConcurrentHashMapを選ぶ
3.CopyOnWriteArrayList の使い所
- 読み取りが多く、書き込みが少ない場合に最適
- 逆は性能が悪い
4.アトミック操作を活用
-
merge(),computeIfPresent(),putIfAbsent()など
5.性能とトレードオフ
- スレッドセーフ = 安全だが遅い
- シングルスレッドなら通常版で十分
関連記事
JavaQuest Part2
📚 参考資料
公式ドキュメント
推奨書籍
- 『Effective Java 第3版』(Joshua Bloch 著)
実務での応用
- Webサーバーでのセッション管理
- データベースのコネクションプール
- リアルタイム通信システム
本記事は教育目的で作成されています。RPGの例を通じてプログラミング概念を楽しく学習できることを目指しています。



