Engineering
Glossvation Engineering
アルゴリズム

Singleton パターン — 2020 年代の Java では `enum` 一択の理由

伝統的な Singleton の 4 実装と、各々のスレッド安全性・シリアライズ耐性・リフレクション耐性。結論として現代は enum Singleton で終わり。

Glossvation ·

Singleton パターン概念図

どういう問題を解くパターンか

プロセス内に唯一のインスタンスを保証する パターン。 典型例は設定レジストリ、接続プール、キャッシュ、ロガーなど 「複数作ると状態が不整合になる」オブジェクトです。

ただし Singleton は副作用の塊 でもあります。

  • グローバル状態になりがちで、テストが書きにくい
  • 依存関係が隠蔽されて、どこで何が共有されているか追いにくい

現代の Java では「DI コンテナにスコープ Singleton で定義する」のがほとんどのケースの正解 です。 素で Singleton を書くのは、DI フレームワークを導入できない制約下に限定しましょう。

4 つの実装と、それぞれの弱点

1. Eager Initialization (起動時に生成)

もっともシンプル。クラスロード時に生成されるためスレッドセーフ。

public final class EagerSingleton {
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    private EagerSingleton() {}

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

弱点: 使わなくても常にメモリに居る。 生成に時間がかかるなら起動が遅くなる。

2. Double-Checked Locking (遅延生成, スレッドセーフ)

public final class LazySingleton {
    private static volatile LazySingleton instance;

    private LazySingleton() {}

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

弱点: volatile を忘れると JIT で命令並べ替えが起き、 「半分だけ初期化された」 インスタンスが見えることがある。 バグが潜みやすい実装。

3. Initialization-on-Demand Holder (推奨: 従来方式)

Bill Pugh のイディオム。遅延 + スレッドセーフを synchronized なしで実現。

public final class HolderSingleton {
    private HolderSingleton() {}

    private static class Holder {
        private static final HolderSingleton INSTANCE = new HolderSingleton();
    }

    public static HolderSingleton getInstance() {
        return Holder.INSTANCE;
    }
}

ネストクラスは 使われたときに初めてロード される性質を利用しています。 2 の DCL より安全で簡潔。

4. Enum Singleton (強く推奨)

Joshua Bloch (Effective Java) が推す書き方。

public enum ConfigRegistry {
    INSTANCE;

    private final Map<String, String> props = new ConcurrentHashMap<>();

    public String get(String key) {
        return props.get(key);
    }

    public void put(String key, String value) {
        props.put(key, value);
    }
}

使うときは:

ConfigRegistry.INSTANCE.put("region", "ap-northeast-1");
String r = ConfigRegistry.INSTANCE.get("region");

なぜ enum が決定版か

攻撃面1〜3 の実装enum
スレッド安全性実装次第言語仕様で保証
シリアライズでインスタンスが増えるreadResolve を書かないと増える保証済み
リフレクションで newInstance されるコンストラクタ呼び出しを防げないIllegalAccessException で阻止
コード量10〜15 行3 行

唯一の弱点 は 「他クラスを継承できない」 こと (enum は暗黙に java.lang.Enum を継承しているため)。 しかし Singleton が他クラスを継承する必要があるケースはほぼ無いので、実務では問題になりません。

落とし穴

1. グローバル状態になりがち

XxxSingleton.getInstance().setX(...) が全コードから呼べると、 どこで誰が状態を変えたか追えなく なります。 テストでは BeforeEach でリセット処理が必要になり、辛い。

2. 本当に 1 プロセス 1 インスタンスか

  • ClassLoader が複数ある環境 (アプリケーションサーバー) では ClassLoader ごと に 1 インスタンス
  • JVM が複数走る環境 (マルチプロセス) では プロセスごと に 1 インスタンス

Kubernetes で Pod が 3 つあれば、Singleton は 3 個存在します。 「分散環境で一意」 を保証したいなら Redis や DB など外部へ出す必要があります。

3. Spring の Singleton スコープとは別物

Spring Bean はデフォルト Singleton スコープですが、 これは コンテナ内 1 インスタンス という意味で、GoF Singleton とは別概念です。 Spring では @Component で宣言するだけ。GoF Singleton を自前で書く必要はありません。

まとめ

  • 新規コードで Singleton が必要になったら まず DI コンテナ を検討
  • どうしても素で書くなら enum 一択
  • 1〜3 の古典実装は、既存コード理解のためだけに覚えておく
  • Singleton を書く前に、「グローバル状態を増やすことに本当に価値があるか」を一度疑う

関連記事