はじめに:synchronizedって「とりあえず付けとけ」じゃない!
Javaを使っていると、マルチスレッド処理の中でよく出てくるキーワードがsynchronized。
「何となく同時アクセスを防ぐために使う」という理解で終わっていませんか?
実は、synchronizedには明確な使いどころと設計のセオリーがあります。
逆に、誤って使うと性能低下やデッドロックなど、実務で深刻な問題を引き起こすことも…。
このブログでは、Java初心者や転職を目指すエンジニアに向けて:
- synchronizedの正体
- 正しい使い方と3つのパターン
- よくある誤用と注意点
- 実務で役立つ設計の考え方
を、図解とコード例でやさしく解説します。
1. synchronizedとは何か?
synchronizedは、Javaで複数スレッドによる同時実行の問題を防ぐためのキーワードです。
主に以下の問題を解決するために使います。
✔ 問題1:競合(Race Condition)
複数のスレッドが同じ変数を同時に更新してしまい、想定外の結果になる。
✔ 問題2:メモリ可視性(Memory Visibility)
あるスレッドが変更した値が、他のスレッドから見えないという問題。
synchronizedは、これらの問題に対してロック(Lock)と可視性保証という2つの機能で対処します。
2. synchronizedの3つの使い方パターン
パターン①:インスタンスメソッドに使う
|
1 2 3 4 |
public synchronized void increment() { count++; } |
- このメソッドに複数スレッドが同時に入ろうとすると、1スレッドずつ順番に実行されます。
- ロック対象は「このインスタンス(this)」。
パターン②:staticメソッドに使う
|
1 2 3 4 |
public static synchronized void increment() { sharedCount++; } |
- ロック対象はクラスそのもの(
ClassName.class)。 - 全インスタンス共通の静的リソースを扱うときに使用。
パターン③:コードブロックに使う
|
1 2 3 4 5 6 |
public void increment() { synchronized(lockObject) { count++; } } |
- ロック対象を任意のオブジェクトにできる。
- 処理の一部だけを同期したいときに便利。
- 性能を考えるなら、必要最小限の範囲で同期すべき!
3. synchronizedで何が起きているのか?
synchronizedが働くとき、Javaは次のようなことをしています。
- 対象オブジェクトの**モニタロック(monitor lock)**を取得
- ロックが取れるまで他のスレッドは待機
- 処理が終わると、ロックを解放
つまり、「一度に一人しか通れない通路」を作ってくれるのがsynchronizedです。
4. 実務での典型的な使い方
✔ カウンターの更新(スレッドセーフな加算)
|
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } } |
複数スレッドからincrement()が呼ばれても、1スレッドずつしか実行されないため、競合が発生しません。
✔ 銀行口座の残高更新(リソースの整合性を守る)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class BankAccount { private int balance = 0; public synchronized void deposit(int amount) { balance += amount; } public synchronized void withdraw(int amount) { if (balance >= amount) { balance -= amount; } } public synchronized int getBalance() { return balance; } } |
複数の操作が絡む場合でも、データの整合性を保つことができます。
5. synchronizedを使うときの注意点
❌ メソッド全体に使いすぎる
|
1 2 3 4 5 |
public synchronized void process() { // 同期の必要がない処理も巻き込んでしまう // 時間がかかる処理が多いと、他スレッドが長時間待機 } |
→ 必要な部分だけを同期ブロックで囲むようにするのがベスト。
❌ ロック対象を誤る
|
1 2 3 4 |
synchronized("lock") { // Stringは使い回される可能性があるので危険 } |
→ private final Object lock = new Object();のように、専用のロックオブジェクトを作るのが安全。
❌ 多重ロックでデッドロックになる
|
1 2 3 |
// Aスレッドがobj1 → obj2 // Bスレッドがobj2 → obj1 |
→ ロック取得順序を統一する、あるいはタイムアウト付きのLockを使うことで回避できます。
6. synchronized以外の手段
Javaには、より柔軟で高性能なロック機構も用意されています。
✔ ReentrantLock
|
1 2 3 4 5 6 7 8 9 |
ReentrantLock lock = new ReentrantLock(); lock.lock(); try { // 排他処理 } finally { lock.unlock(); } |
- 明示的なロック/解除が必要
- try-finallyで忘れずにunlock!
✔ AtomicInteger などのAtomic系クラス
|
1 2 3 |
AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); |
- 軽量で高速な排他処理
- カウンターなど単純なケースで最適
7. パフォーマンスを考慮した使い方
- 同期ブロックの範囲は最小限に!
- ボトルネックになりそうな処理にはAtomic系を検討
- 複数スレッドから同時に読み込むだけなら、読み取りに同期は不要
8. まとめ:synchronizedは設計力を問われる機能
synchronizedは簡単に使えるように見えて、実は設計ミスが致命傷になる機能です。
- 「どこをロックすべきか?」
- 「どのオブジェクトにロックをかけるか?」
- 「ロック範囲は適切か?」
- 「より適した代替手段はないか?」
こうした判断ができるようになれば、あなたは一人前のJavaプログラマーです!
スキルアップのおすすめ
もっとJavaの並行処理・設計・例外処理なども深く学びたい方は、
👉 絶対にJavaプログラマーになりたい人へ。
で基本をおさえ、
👉 サイゼントアカデミー
で設計レビューやコードの添削を受けながら、実力をつけていきましょう!

コメント