👻

【Java Gold】synchronizedとReentrantLockの整理

に公開

はじめに

synchronizedReentrantLockの使い分けに不安があったため、ここで基本を整理します。

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() 毎回異なるため意味なし ❌ 無意味なロックになる
IntegerStringなどのラッパー 変更不可かつキャッシュ特性がある ❌ 予期せぬ共有の危険あり
private final Object lock = new Object(); 推奨される方法 安定した排他制御が可能

ロック対象が途中で変わると無効

synchronized(lockObj.num) { ... }  // ❌ numが変更されると別オブジェクトに
  • ロック対象はfinalで固定すべき

ロックの粒度

粒度 メリット デメリット
広い(メソッド全体) コードが簡単 同時実行性が低くなる
狭い(必要最小限) 同時実行性が高くなる コードが複雑になりやすい
  • 原則として「必要な範囲のみ」をロックするのがベスト

デッドロックに注意

  • 複数ロックを異なる順序で取得すると、デッドロック(相互待機)になる

  • 対策:

    • ロック取得の順番を常に統一する
    • ReentrantLocktryLockでタイムアウト制御

再入可能性

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は細かな制御や、割り込み・タイムアウト処理をしたいときに最適

Discussion