java-recipes

ホーム マルチスレッド › Th-06

Th-06: ReentrantLock / ReadWriteLock

synchronized はシンプルですが、タイムアウト指定やロック取得の試みといった細かい制御はできません。ReentrantLock を使うと tryLock() でタイムアウトを設定でき、ReadWriteLock を使うと読み取りと書き込みを分離してパフォーマンスを向上させられます。

synchronized との違い

synchronized はシンプルで使いやすい反面、ロック待ちが無限に続く可能性があります。ReentrantLocktryLock(timeout) でタイムアウトを設定でき、 指定時間内にロックを取れなければ別の処理に切り替えることができます。 また、公平モード(順番を守る)の設定も可能です。

機能synchronizedReentrantLock
基本的なロック
タイムアウト付き tryLock
公平モード(FIFO)✅(new ReentrantLock(true))
読み書き分離✅(ReadWriteLock)
記述のシンプルさ△(unlock() を finally に書く必要あり)

ReadWriteLock の読み書き分離

キャッシュのように「読み取りは頻繁・書き込みはまれ」という場面では、ReadWriteLock が効果的です。 読み取りロックは複数スレッドが同時に取得できますが、書き込みロックは1スレッドのみが排他的に取得します。 これにより読み取り専用のアクセスをブロックせず、スループットを高められます。

ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock  = rwLock.readLock();   // 複数スレッドが同時取得可
Lock writeLock = rwLock.writeLock();  // 1スレッドのみ取得可

// 読み取り: 複数スレッドが同時に実行できる
readLock.lock();
try { /* データを読む */ } finally { readLock.unlock(); }

// 書き込み: 他のすべてのスレッドが待機する
writeLock.lock();
try { /* データを更新 */ } finally { writeLock.unlock(); }

サンプルコード

Sample.java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.*;

public class ReentrantLockSample {

    // ✅ ReentrantLock: synchronized より細粒度な制御
    static class SafeCounterWithLock {
        private int count = 0;
        private final Lock lock = new ReentrantLock();

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

        // タイムアウト付きロック取得
        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();
        private final Lock readLock = rwLock.readLock();
        private final Lock writeLock = rwLock.writeLock();

        // 読み取りは並行可
        public String read() {
            readLock.lock();
            try {
                System.out.println("[読み取り] " + Thread.currentThread().getName()
                    + " → " + data);
                return data;
            } finally {
                readLock.unlock();
            }
        }

        // 書き込みは排他的
        public void write(String newData) {
            writeLock.lock();
            try {
                System.out.println("[書き込み] " + Thread.currentThread().getName()
                    + " → " + newData);
                data = newData;
            } finally {
                writeLock.unlock();
            }
        }
    }

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

        System.out.println("\n=== ReadWriteLock ===");
        CachedData cache = new CachedData();

        // 複数スレッドで同時読み取り(並行可)
        Thread r1 = new Thread(new Runnable() {
            @Override
            public void run() { cache.read(); }
        }, "reader-1");
        Thread r2 = new Thread(new Runnable() {
            @Override
            public void run() { cache.read(); }
        }, "reader-2");
        Thread w1 = new Thread(new Runnable() {
            @Override
            public void run() { cache.write("更新データ"); }
        }, "writer-1");

        r1.start(); r2.start();
        r1.join(); r2.join();
        w1.start(); w1.join();
        cache.read();
    }
}

よくあるミス・注意点

⚠️ lock() 後に unlock() を finally に書かないと例外でロックが解放されない

lock() を呼んだ後、処理中に例外が発生すると unlock() が呼ばれずロックが永遠に保持されます。 他のスレッドはロックを取得できなくなり、デッドロック状態になります。unlock() は必ず finally ブロックに書いてください。

// ❌ finally なし: 例外が起きるとロックが解放されない
lock.lock();
count++; // ここで例外が起きると unlock() が呼ばれない
lock.unlock();

// ✅ finally に unlock() を書く
lock.lock();
try {
    count++;
} finally {
    lock.unlock(); // 例外が起きても必ず解放される
}

⚠️ 読み取りロックを保持したまま書き込みロックを取得しようとするとデッドロックになる

ReadWriteLock では、同一スレッドが読み取りロックを保持したまま書き込みロックを取得しようとするとデッドロックになります。 読み取りロックを解放してから書き込みロックを取得するか、最初から書き込みロックを使ってください。

テストする観点

  • ReentrantLock で 5スレッド × 1000回インクリメントした結果が必ず 5000 になること
  • tryLock(timeoutMs) で指定時間内にロックを取れない場合に false が返ること
  • ReadWriteLock で複数の読み取りスレッドが同時実行できること(書き込みスレッドはブロックされること)
  • ✅ 書き込み中は読み取りスレッドが待機し、書き込み完了後に新しい値を読み取れること
  • unlock() を呼ばずに例外が起きた場合、他スレッドがロックを永遠に待ち続けること(finally の重要性確認)

GitHub でソースコードを見る →