概要

synchronized はシンプルで扱いやすい排他制御ですが、ロック取得にタイムアウトを設けたい、読み取りは並行で許可したいといった要件には対応できません。java.util.concurrent.locks パッケージの ReentrantLock と ReadWriteLock は、こうした場面で synchronized の代わりに使える柔軟なロック機構です。この記事では、ReentrantLock の基本的な lock/unlock パターンから tryLock によるタイムアウト付きロック取得、ReadWriteLock による読み取り並行・書き込み排他の実装まで、実務で使いどころの多いパターンを整理します。finally での unlock を忘れたときの影響もあわせて確認します。

使いどころ

キャッシュの読み取りは複数スレッドで並行に許可し、更新時だけ排他ロックをかけて整合性を保つ

外部システム連携で tryLock を使い、一定時間内にロックが取れなければリトライやエラーハンドリングに回す

バッチの進捗テーブル更新で ReentrantLock を使い、同一レコードへの同時書き込みを防止する

コード例

ReentrantLockDemo.java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantLockDemo {

    // ReentrantLock によるスレッドセーフカウンター
    static class SafeCounter {
        private int count = 0;
        private final Lock lock = new ReentrantLock();

        public void increment() {
            lock.lock();
            try {
                count++;
            } finally {
                lock.unlock(); // 必ず finally で解放
            }
        }

        public boolean tryIncrement(long timeoutMs) throws InterruptedException {
            if (lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
                try {
                    count++;
                    return true;
                } finally {
                    lock.unlock();
                }
            }
            return false;
        }

        public int getCount() { return count; }
    }

    // ReadWriteLock によるキャッシュ
    static class CachedData {
        private String data = "初期データ";
        private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

        public String read() {
            rwLock.readLock().lock();
            try {
                return data;
            } finally {
                rwLock.readLock().unlock();
            }
        }

        public void write(String newData) {
            rwLock.writeLock().lock();
            try {
                data = newData;
            } finally {
                rwLock.writeLock().unlock();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        var counter = new SafeCounter();
        var threads = new Thread[5];
        for (var i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                for (var j = 0; j < 1000; j++) counter.increment();
            });
            threads[i].start();
        }
        for (var t : threads) t.join();
        System.out.println("結果: " + counter.getCount() + " (期待: 5000)");

        // ReadWriteLock: 読み取りは並行、書き込みは排他
        var cache = new CachedData();
        var r1 = new Thread(() ->
            System.out.println("[reader-1] " + cache.read()), "reader-1");
        var r2 = new Thread(() ->
            System.out.println("[reader-2] " + cache.read()), "reader-2");
        r1.start(); r2.start();
        r1.join(); r2.join();

        var w1 = new Thread(() -> cache.write("更新データ"), "writer-1");
        w1.start(); w1.join();
        System.out.println("[main] " + cache.read());
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var と record を組み合わせ、ロック結果を LockResult(boolean acquired, int value) のように型安全に扱える。

Java 17
// Java 17: record でロック結果を値として返す
record LockResult(boolean acquired, int value) {}
// tryLock 成功時
return new LockResult(true, count);
// 失敗時
return new LockResult(false, count);

Library Comparison

ReentrantLockタイムアウト付きロック取得、公平性の設定、Condition による条件待ちが必要なとき。lock/unlock の対を自分で管理する必要がある。synchronized より書くべきコードが増え、unlock 忘れのリスクがある。
synchronizedブロックを抜ければ自動解放される簡易なロックで十分なとき。コードの簡潔さを優先する場面。タイムアウトや条件待ちの機能がない。ロック粒度の細かい制御もやりにくい。
StampedLock読み取りが圧倒的に多く、楽観的読み取り(optimistic read)で性能を最大化したいとき。API が複雑で誤用のリスクが高い。再入可能(reentrant)ではないため、再帰呼び出しがあるとデッドロックする。

注意点

lock() を呼んだら必ず finally ブロックで unlock() する。例外発生時に unlock が漏れるとデッドロックの原因になる

tryLock() の戻り値を確認せずにクリティカルセクションに入るとロックなしで共有データを操作してしまう

ReadWriteLock の readLock 内で writeLock を取ろうとするとデッドロックになる。ロックのアップグレードは標準 API ではサポートされていない

ReentrantLock は synchronized と異なり、ロックの解放を開発者が管理する責任を負う。コードレビューで unlock 漏れを重点的にチェックすべき

FAQ

synchronized と ReentrantLock のどちらを先に検討すべきですか。

まず synchronized で十分かを検討します。タイムアウトや Condition が必要な場合のみ ReentrantLock に切り替えるのが、コードの簡潔さを保つ方針です。

ReadWriteLock は読み取りが多い場面で常に有利ですか。

読み取りが大半を占める場面では有利ですが、書き込みが頻繁だと writeLock の取得待ちが増え、ReentrantLock と大差なくなります。読み書き比率で判断してください。

ReentrantLock のコンストラクタで fair=true を指定すべきですか。

公平性ありにすると待機時間の長いスレッドが優先されますが、スループットは低下します。スレッド飢餓が問題になる場合のみ true にしてください。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます