はじめに
synchronized
とReentrantLock
の使い分けに不安があったため、ここで基本を整理します。
synchronizedの基本
排他制御とは?
複数のスレッドが同じ処理・データに同時にアクセスしないようにする仕組みのことです。
Javaではsynchronized
を使うことで、簡単に排他制御を行えます。
synchronizedの使い方
メソッドに指定(インスタンスメソッド)
public synchronized void increment() {
count++;
}
- このメソッドはインスタンス単位で排他制御されます(
this
にロックがかかる)。
staticメソッドに指定(クラスロック)
public static synchronized void log(String msg) {
// クラス全体に対してロック
}
- クラス全体にロック(
クラス名.class
)がかかります。 - 全インスタンス共通のデータを扱うときに使います。
ブロックに指定(より細かく制御)
public void add(int value) {
synchronized(this) {
list.add(value);
}
}
public void add(int value) {
synchronized(lockObj) {
list.add(value);
}
}
- 処理の一部にだけロックをかけたいときに有効です。
synchronizedの動作原理
-
synchronized
ブロックに入るときに、対象オブジェクトのモニタロックを取得 - 処理が終わるとロックを自動的に解放
- 他スレッドはロックが解放されるまで待機
synchronizedの注意点
ロック対象の選び方に注意
ロック対象 | 説明 | 注意点 |
---|---|---|
this |
インスタンス単位の排他制御 | インスタンスが異なれば無効 |
MyClass.class |
クラス単位の排他制御 | staticデータ用 |
new Object() |
毎回異なるため意味なし | ❌ 無意味なロックになる |
Integer やString などのラッパー |
変更不可かつキャッシュ特性がある | ❌ 予期せぬ共有の危険あり |
private final Object lock = new Object(); |
推奨される方法 | 安定した排他制御が可能 |
ロック対象が途中で変わると無効
synchronized(lockObj.num) { ... } // ❌ numが変更されると別オブジェクトに
- ロック対象は
final
で固定すべき
ロックの粒度
粒度 | メリット | デメリット |
---|---|---|
広い(メソッド全体) | コードが簡単 | 同時実行性が低くなる |
狭い(必要最小限) | 同時実行性が高くなる | コードが複雑になりやすい |
- 原則として「必要な範囲のみ」をロックするのがベスト
デッドロックに注意
-
複数ロックを異なる順序で取得すると、デッドロック(相互待機)になる
-
対策:
- ロック取得の順番を常に統一する
-
ReentrantLock
のtryLock
でタイムアウト制御
再入可能性
synchronized
は再入可能です。
同一スレッドが再度同じロックを取得することができます。
public synchronized void methodA() {
methodB(); // 同じロックを保持しているためOK
}
スレッド競合のある例と解決
public class Counter {
private int count = 0;
public void increment() {
count++; // スレッドセーフでない
}
public int getCount() {
return count;
}
}
上記はスレッドセーフではありません。
synchronized
を使って以下のように修正します。
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
ReentrantLockとは
java.util.concurrent.locks.ReentrantLock
は、synchronized
と同様の排他制御を明示的に制御できるクラスです。
- 再入可能(同じスレッドが複数回ロック取得可能)
- ロック取得/解放を手動で管理するため、細かい制御が可能
ReentrantLockの使い方
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
-
lock()
でロックを取得し、必ずunlock()
で解放(finally
ブロックで書くのが鉄則)
ReentrantLockのインスタンス生成場所
インスタンスフィールドとして定義
private final ReentrantLock lock = new ReentrantLock();
- 同じオブジェクト内の複数メソッドで共有可能
- 一番一般的かつ安全なパターン
staticフィールドに定義(クラス全体で共有)
private static final ReentrantLock lock = new ReentrantLock();
- 全インスタンスで共有されるロック(staticフィールドやstaticメソッドを守るとき)
メソッド内でnew
public void increment() {
ReentrantLock lock = new ReentrantLock(); // ❌ 無意味
}
- 毎回異なるロックになるため、排他制御が意味をなさない
外部から注入する
public class Service {
private final ReentrantLock lock;
public Service(ReentrantLock lock) {
this.lock = lock;
}
}
- 柔軟な設計や複数オブジェクトで同じロックを使いたい場合に有効
synchronizedとReentrantLockの比較
比較項目 | synchronized | ReentrantLock |
---|---|---|
ロックの種類 | 暗黙的(モニタロック) | 明示的(コードで管理) |
ロック管理 | 自動 | 手動(lock/unlock) |
tryLock | 不可 | 可能(ロック失敗時の分岐が可能) |
割り込み対応 | 不可 |
lockInterruptibly() で可能 |
公平性制御 | 不可 | コンストラクタで設定可能(先に待っていたスレッド優先) |
まとめ
-
synchronized
はシンプルで安全、ロックが短時間かつスコープが明確なときに便利 -
ReentrantLock
は細かな制御や、割り込み・タイムアウト処理をしたいときに最適