概要

マルチスレッド環境で共有データを操作すると、読み書きのタイミングが重なって結果が壊れるという問題に直面します。Java の synchronized キーワードは、この排他制御を言語レベルで実現する最も基本的な仕組みです。ただし、メソッド全体にかけるのかブロック単位にするのか、ロック対象を this にするのか専用オブジェクトにするのかで、性能と安全性が変わります。この記事では、スレッドアンセーフなカウンターで競合を実際に発生させたうえで、synchronized メソッドと synchronized ブロックの2つの書き方を比較します。実務でロック粒度の判断に迷ったときの指針を示します。

使いどころ

在庫数の加減算を複数スレッドから同時に行うバッチで、カウントのズレを防止する

共有キャッシュへの読み書きを排他制御し、不整合なデータの読み出しを防ぐ

ログファイルへの書き込みを synchronized ブロックで保護し、行の混在を防止する

コード例

SynchronizedDemo.java
public class SynchronizedDemo {

    // synchronized ブロック + 専用ロックオブジェクト(推奨)
    static class SafeCounter {
        private int count = 0;
        private final Object lock = new Object();

        public void increment() {
            synchronized (lock) {
                count++;
            }
        }

        public int getCount() {
            synchronized (lock) {
                return count;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        var counter = new SafeCounter();
        var threadCount = 10;
        var incrementsPerThread = 1000;

        var threads = new Thread[threadCount];
        for (var i = 0; i < threadCount; i++) {
            threads[i] = new Thread(() -> {
                for (var j = 0; j < incrementsPerThread; j++) {
                    counter.increment();
                }
            });
            threads[i].start();
        }
        for (var t : threads) {
            t.join();
        }

        var expected = threadCount * incrementsPerThread;
        var actual = counter.getCount();
        System.out.printf("期待値=%d, 実際=%d, 一致=%b%n",
            expected, actual, expected == actual);
    }
}

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

Version Coverage

パターンマッチング instanceof(Java 16+)でキャスト不要。var で型推論を活用できる。

Java 17
// Java 17: パターンマッチング instanceof
if (counter instanceof UnsafeCounter c) {
    c.increment();
} else if (counter instanceof SafeCounter c) {
    c.increment();
}

Library Comparison

synchronizedロック範囲が明確で、タイムアウトや条件待ちが不要なシンプルな排他制御。ロック取得の試行やタイムアウトが設定できない。ReentrantLock より機能は限定的だが、書き間違いが少ない。
ReentrantLocktryLock でタイムアウト付きロック取得や、Condition による条件待ちが必要なとき。lock/unlock の対を自分で管理する必要があり、finally での unlock を忘れるとデッドロックに直結する。
AtomicInteger単純なカウンタやフラグなど、単一変数のアトミック操作で済む場合。複数変数にまたがる一貫性保証はできない。複合操作にはロックが必要。

注意点

synchronized メソッドは this をロック対象にするため、同じインスタンスの他の synchronized メソッドもブロックされる。粒度を細かくしたい場合は専用ロックオブジェクトを使う

count++ は読み取り・加算・書き戻しの3ステップで実行される非アトミック操作。volatile を付けただけでは競合を防げない

ロック対象に this を使うと、外部からそのインスタンスで synchronized を取られるリスクがある。private final Object lock = new Object() で専用ロックにするのが安全

synchronized 内で長時間の I/O 操作を行うとスループットが大幅に低下する。ロック範囲は最小限にとどめる

FAQ

synchronized メソッドとブロックのどちらを使うべきですか。

ロック範囲を最小限にできる synchronized ブロックが基本です。メソッド全体をロックする必要がない場面で synchronized メソッドを使うと、不要なブロッキングが発生します。

static メソッドに synchronized を付けるとどうなりますか。

ロック対象がインスタンスではなくクラスオブジェクト(Class<?>)になります。全インスタンスで共有されるため影響範囲が広く、意図しない待ちが発生しやすいです。

synchronized の中で例外が発生した場合、ロックは解放されますか。

はい、synchronized ブロックを抜ける際にロックは自動的に解放されます。ReentrantLock と異なり、finally での明示的な unlock は不要です。

関連書籍

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

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