Skip to content

Seulah-coder/tdd-java

Repository files navigation

同時実行性競合(Race Condition)に関する報告書

1️⃣ 同時実行性競合が発生する理由

同時実行性競合(Race Condition)とは、複数のスレッドが同じデータに同時に読み書きすることで、予期せぬ結果が生じる状況を指します。

例えば、ポイントチャージサービスにおいて以下のコードがあるとします。

long current = userPointTable.selectById(userId).point(); // ①読み取り
long updated = current + amount;                          // ②計算
userPointTable.insertOrUpdate(userId, updated);           // ③書き込み

同じユーザーに複数スレッドが同時にポイントをチャージする場合、衝突が発生します。

  • 両方とも①読み取りで現在のポイントが500と取得
  • スレッドAは100を加算して600を計算、スレッドBも同じく600を計算
  • スレッドAが先にDB/メモリに保存 → 600
  • スレッドBがその後に保存 → 600
スレッド 読み取り(current) 計算(updated) 書き込み(DB反映)
Thread A 500 500 + 100 = 600 600
Thread B 500 500 + 100 = 600 600

✅ 結果: 本来 500 + 100 + 100 = 700 のはずが、最終DB値は 600 → 同時性衝突発生


2️⃣ 競合が発生する典型的なケース

同時実行性競合は、主に「読み取りと書き込みが混ざった操作(Read-Modify-Write)」で発生します。

  • 単純な読み取り(read-only):安全、競合なし
  • 読み取り + 計算 + 書き込み(read-modify-write):危険、競合の可能性あり

実際の例

  • 銀行口座の振込
  • ポイントのチャージ/利用
  • 在庫数の減算
  • 統計カウンターの増加

3️⃣ Javaで競合が発生する理由

  • JVMのメモリモデルにより、スレッドごとにCPUキャッシュ上に値が存在する場合がある
  • HashMapなどの通常のデータ構造は同時アクセスを保護しない

そのため、読み書き中に他スレッドが同時にアクセスすると、値が上書きされることになります。


4️⃣ 競合を防ぐ方法

① synchronized

synchronized(userIdLock) {
    long current = userPointTable.selectById(userId).point();
    long updated = current + amount;
    userPointTable.insertOrUpdate(userId, updated);
}

## 2️⃣ Synchronized / per-user Lockによる解決

各ユーザーごとにロックを取得して順序制御することで衝突を回避できます。

### スレッドごとの処理

| スレッド | 動作 |
|----------|------|
| Thread A | lock取得readcalculatewritelock解放 |
| Thread B | lock待機Thread A完了後 lock取得readcalculatewritelock解放 |

② ReentrantLock

lock.lock();
try {
    long current = userPointTable.selectById(userId).point();
    long updated = current + amount;
    userPointTable.insertOrUpdate(userId, updated);
} finally {
    lock.unlock();
}

- 各ユーザーIDごとに独立したロックを管理
- 複数ユーザーは同時にチャージ可能
- 同一ユーザーは順序通りに処理される

User1 Lock Map:
{ userId=1 -> LOCK }

Thread A: lock(userId=1) -> read -> update -> write -> unlock
Thread B: lock(userId=1) -> read -> update -> write -> unlock (待機)
Thread C: lock(userId=2) -> read -> update -> write -> unlock (同時処理可能)

③ Atomic演算 / CAS

  • AtomicLongなどの原子型を利用
  • 上書きではなく Compare-And-Swap(CAS) により値を更新

④ ConcurrentHashMap

  • 複数スレッドが異なるキーに同時アクセス可能
  • 特定キーに対してのみ lock や atomic 処理が可能

結果

pool-2-thread-50 実行開始
pool-2-thread-50 実行終了: 100
pool-2-thread-41 実行開始
pool-2-thread-41 実行終了: 200
pool-2-thread-42 実行開始
pool-2-thread-42 実行終了: 300
pool-2-thread-46 実行開始
pool-2-thread-46 実行終了: 400
pool-2-thread-45 実行開始
pool-2-thread-45 実行終了: 500
pool-2-thread-44 実行開始
pool-2-thread-44 実行終了: 600
pool-2-thread-43 実行開始
pool-2-thread-43 実行終了: 700
pool-2-thread-48 実行開始
pool-2-thread-48 実行終了: 800
pool-2-thread-49 実行開始
pool-2-thread-49 実行終了: 900
pool-2-thread-47 実行開始
pool-2-thread-47 実行終了: 1000
pool-2-thread-17 実行開始
pool-2-thread-17 実行終了: 1100
pool-2-thread-19 実行開始
pool-2-thread-19 実行終了: 1200
pool-2-thread-24 実行開始
pool-2-thread-24 実行終了: 1300
pool-2-thread-25 実行開始
pool-2-thread-25 実行終了: 1400
pool-2-thread-27 実行開始
pool-2-thread-27 実行終了: 1500
pool-2-thread-38 実行開始
pool-2-thread-38 実行終了: 1600
pool-2-thread-29 実行開始
pool-2-thread-29 実行終了: 1700
pool-2-thread-30 実行開始
pool-2-thread-30 実行終了: 1800
pool-2-thread-32 実行開始
pool-2-thread-32 実行終了: 1900
pool-2-thread-31 実行開始
pool-2-thread-31 実行終了: 2000
pool-2-thread-33 実行開始
pool-2-thread-33 実行終了: 2100
pool-2-thread-34 実行開始
pool-2-thread-34 実行終了: 2200
pool-2-thread-35 実行開始
pool-2-thread-35 実行終了: 2300
pool-2-thread-37 実行開始
pool-2-thread-37 実行終了: 2400
pool-2-thread-28 実行開始
pool-2-thread-28 実行終了: 2500
pool-2-thread-39 実行開始
pool-2-thread-39 実行終了: 2600
pool-2-thread-40 実行開始
pool-2-thread-40 実行終了: 2700
pool-2-thread-8 実行開始
pool-2-thread-8 実行終了: 2800
pool-2-thread-4 実行開始
pool-2-thread-4 実行終了: 2900
pool-2-thread-6 実行開始
pool-2-thread-6 実行終了: 3000
pool-2-thread-9 実行開始
pool-2-thread-9 実行終了: 3100
pool-2-thread-36 実行開始
pool-2-thread-36 実行終了: 3200
pool-2-thread-10 実行開始
pool-2-thread-10 実行終了: 3300
pool-2-thread-11 実行開始
pool-2-thread-11 実行終了: 3400
pool-2-thread-12 実行開始
pool-2-thread-12 実行終了: 3500
pool-2-thread-22 実行開始
pool-2-thread-22 実行終了: 3600
pool-2-thread-21 実行開始
pool-2-thread-21 実行終了: 3700
pool-2-thread-26 実行開始
pool-2-thread-26 実行終了: 3800
pool-2-thread-16 実行開始
pool-2-thread-16 実行終了: 3900
pool-2-thread-14 実行開始
pool-2-thread-14 実行終了: 4000
pool-2-thread-13 実行開始
pool-2-thread-13 実行終了: 4100
pool-2-thread-18 実行開始
pool-2-thread-18 実行終了: 4200
pool-2-thread-20 実行開始
pool-2-thread-20 実行終了: 4300
pool-2-thread-23 実行開始
pool-2-thread-23 実行終了: 4400
pool-2-thread-2 実行開始
pool-2-thread-2 実行終了: 4500
pool-2-thread-15 実行開始
pool-2-thread-15 実行終了: 4600
pool-2-thread-5 実行開始
pool-2-thread-5 実行終了: 4700
pool-2-thread-3 実行開始
pool-2-thread-3 実行終了: 4800
pool-2-thread-1 実行開始
pool-2-thread-1 実行終了: 4900
pool-2-thread-7 実行開始
pool-2-thread-7 実行終了: 5000

最終ポイント: 5000

💡 まとめ

Javaにおける同時実行性競合は、読み取り → 計算 → 書き込み(Read-Modify-Write) の操作を複数スレッドが同時に行うことで発生します。

対策として、ユーザーごとのロック、synchronizedReentrantLock、Atomic操作などを活用して、競合を防止することが重要です。

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages