スレッドセーフなシングルトンパターンの実装手法

シングルトンパターンは、インスタンスが1つだけ生成されることを保証するデザインパターンです。しかし、マルチスレッド環境では、単純な実装では複数のインスタンスが生成されるリスクがあります。本記事では、スレッドセーフなシングルトンの実装方法を、具体的なコード例とともに解説します。

1. スレッドアンセーフな基本実装

以下のコードは、典型的な「遅延初期化(Lazy Initialization)」を採用したシングルトンです。複数のスレッドが同時に getInstance() を呼び出すと、条件分岐を同時に通過し、複数のインスタンスが生成される可能性があります。

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

2. スレッドセーフな事前生成(Eager Initialization)

最も単純なスレッドセーフ対策は、クラスロード時にインスタンスを生成する方法です。これにより、getInstance() は競合状態を起こさずに常に同じインスタンスを返します。ただし、インスタンスが実際に使用される前に生成されるため、メモリ使用量に注意が必要です。

public class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

3. synchronized を使用したスレッドセーフ化

遅延初期化を維持しつつ、synchronized キーワードをメソッド全体に適用することでスレッドセーフを実現できます。しかし、すべてのスレッドがこのメソッドを呼び出すたびに同期オーバーヘッドが発生するため、パフォーマンスに影響を与えます。

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

4. ダブルチェックロッキング(Double-Checked Locking)

同期のオーバーヘッドを削減するために、インスタンスが null の場合のみ同期ブロックに入る手法です。変数に volatile キーワードを付与することで、インスタンスの可視性を保証し、部分構築オブジェクトの問題を防止します。

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

5. 静的内部クラスを利用した方法

この手法では、静的内部クラス(Initialization-on-demand holder idiom)を利用して、インスタンスの生成を遅延させつつスレッドセーフを実現します。外部クラスがロードされても内部クラスは即座にロードされず、getInstance() の初回呼び出し時にのみ内部クラスがロードされ、インスタンスが生成されます。

public class Singleton {
    private Singleton() {}

    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

内部クラスによるスレッドセーフの仕組み

Java 仮想マシン(JVM)におけるクラスの初期化は、スレッドセーフに行われることが保証されています。静的内部クラスは受動的参照(Passive Reference)に該当するため、getInstance() が呼び出されるまでロードされません。

クラスの初期化が発生する条件(アクティブ参照)は、以下の5つに限定されています(JLS および JVM 仕様に基づく)。

  • new によるインスタンス生成、静的フィールドの読み書き(final かつコンパイル時定数は除く)、静的メソッドの呼び出し
  • java.lang.reflect によるリフレクション呼び出し
  • サブクラスの初期化に伴う親クラスの初期化
  • JVM 起動時のメインクラス(main メソッドを持つクラス)の初期化
  • Java 7 以降の java.lang.invoke.MethodHandle による静的メソッドの間接呼び出し

静的内部クラスはこれらに該当しないため、SingletonHolder のクラスロードと初期化は getInstance() 呼び出し時に行われ、その初期化プロセスは JVM によってスレッドセーフに管理されます。その結果、同期や volatile 変数を使用せずとも、安全で効率的なシングルトンが実現できます。

タグ: シングルトンパターン スレッドセーフ 遅延初期化 ダブルチェックロッキング Volatileキーワード

7月4日 16:15 投稿