【Java学習|実務向け】現代のJava並行処理におけるReentrantLockの正しい使いどころ

1. 導入

Javaの並行処理において、synchronizedキーワードは最も手軽な排他制御の手段ですが、柔軟性に欠けるという課題があります。例えば、ロックの取得を待機中に中断したい場合や、タイムアウトを設定したい場合、あるいは「読み込みは複数許可し、書き込みは排他する」といった複雑な制御には向きません。本記事では、java.util.concurrent.locks.ReentrantLockを用いて、より堅牢でスケーラブルな並行処理を実現する方法を解説します。

2. 基礎知識

ReentrantLockは、java.util.concurrent.locks.Lockインターフェースを実装した「再入可能」な排他ロックです。
再入可能(Reentrant)とは、あるスレッドが既にロックを獲得している場合、同じスレッドが再びそのロックを要求してもブロックされずに通過できる性質を指します。これにより、同じクラス内のメソッドを呼び出す際、デッドロックに陥るリスクを回避できます。
また、synchronizedブロックと異なり、ロックの取得と解放を異なるブロック間で行えるため、複雑なアルゴリズムの実装に適しています。

3. 実装/解決策

ReentrantLockを使用する際の鉄則は、try-finallyブロックで確実にunlockすることです。例外が発生しても必ずロックが解放されるように記述しなければ、デッドロックの原因となります。
また、近年導入されたVirtual Threads環境下では、ReentrantLockは実行中のキャリアスレッドをブロックする可能性があるため、短時間の処理に限定して使用することを推奨します。

4. サンプルプログラム

以下のコードは、共有リソースへのアクセスをReentrantLockで保護する標準的な実装例です。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SharedResource {
    // ReentrantLockのインスタンス化
    private final Lock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        // ロックの取得
        lock.lock();
        try {
            // クリティカルセクション(排他が必要な処理)
            count++;
            System.out.println("現在のカウント: " + count);
        } finally {
            // 異常終了時も確実にロックを解放するためにfinally句で呼ぶ
            lock.unlock();
        }
    }

    public boolean tryIncrementWithTimeout() {
        // タイムアウト付きのロック試行(待機時間の制御が可能)
        try {
            if (lock.tryLock(500, java.util.concurrent.TimeUnit.MILLISECONDS)) {
                try {
                    count++;
                    return true;
                } finally {
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return false; // ロック取得失敗
    }
}

5. 応用・注意点

現場でReentrantLockを使用する際、以下の点に注意してください。

1. tryLockの活用:
デッドロックを回避するため、常にロックし続けるのではなく、tryLockを使用して一定時間で諦める設計を取り入れることが、システムの応答性向上に寄与します。

2. Virtual Threadsとの相性:
Java 21以降のVirtual Threads環境では、ReentrantLockを使用するとキャリアスレッドがピン留め(pinned)され、Virtual Threadsのメリットである高いスケーラビリティが損なわれる場合があります。可能であれば、java.util.concurrent.atomicパッケージ(AtomicInteger等)や、新しい構造化並行処理(Structured Concurrency)による設計を優先的に検討してください。

3. 公平性の設定:
ReentrantLockのコンストラクタで「true」を指定すると、待機時間が長いスレッドから優先的にロックを取得する「公平モード」になります。ただし、これはスループットを低下させるため、極端な飢餓状態が懸念されない限り、デフォルト(非公平)で使用するのがベストプラクティスです。

コメント

タイトルとURLをコピーしました