Th-02: synchronized キーワード
複数のスレッドが同じデータを同時に読み書きすると、意図しない値になる「レースコンディション」が発生します。synchronized を使ってロックをかけることで、一度に1つのスレッドだけがアクセスできるようにする方法を解説します。
レースコンディションとは
count++ は一見1行の命令ですが、実際にはCPUレベルで「読み取り → 加算 → 書き戻し」の3ステップで実行されます。 スレッド A が読み取った直後にスレッド B も同じ値を読み取ると、2回 ++ したはずが1しか増えません。 これが「レースコンディション(競合状態)」です。
// count = 5 の状態で2スレッドが同時実行する場合 スレッドA: count を読み取る (5) スレッドB: count を読み取る (5) ← A がまだ書き戻す前に読んでしまう スレッドA: 5 + 1 = 6 を書き戻す スレッドB: 5 + 1 = 6 を書き戻す ← 期待は 7 だが 6 になってしまう
synchronized の2つの使い方
| 方法 | 書き方 | ロック対象 | 推奨度 |
|---|---|---|---|
| synchronized メソッド | public synchronized void increment() | this(インスタンス全体) | ○ シンプル |
| synchronized ブロック | synchronized(lock) { ... } | 専用ロックオブジェクト | ◎ ロック範囲を最小化できる |
synchronized ブロックを使うと、ロックする範囲(クリティカルセクション)を最小限に絞れます。 ロック範囲が大きいほど並行処理の恩恵が減るため、必要な箇所だけロックする書き方が推奨されます。
サンプルコード
よくあるミス・注意点
⚠️ synchronized をつけ忘れてレースコンディションが起きる
increment() に synchronized をつけても getCount() につけ忘れると、 不完全な値が返ることがあります。読み取り側も同じロックで保護する必要があります。
⚠️ 複数のロックオブジェクトを混在させて意図しない競合が起きる
synchronized(this) と synchronized(lock) が同じクラスの別メソッドで使われていると、 それぞれ別のロックで保護されます。同じフィールドを守るロックは統一してください。
⚠️ ロックの粒度が大きすぎてパフォーマンスが低下する
メソッド全体に synchronized をつけると、時間のかかる処理(ファイルI/O や HTTP 通信など)もロックされてしまいます。 ロックが必要なのはデータを変更する最小限の箇所だけです。synchronized ブロックで範囲を絞ることを検討してください。
テストする観点
- ✅
SafeCounterMethodで 10スレッド × 1000回インクリメントした結果が 10000 になること - ✅
SafeCounterBlockで同様に 10000 になること - ✅
UnsafeCounterでは複数回実行すると結果が 10000 にならない(確率的にずれる)こと - ✅
increment()にsynchronizedをつけてもgetCount()に付け忘れた場合、読み取り値が不正になる可能性があること - ✅ ロックオブジェクトが異なる2つの
synchronizedブロックは互いに干渉しないこと