概要

アプリケーション全体でインスタンスを1つだけ保持したい場面は、設定管理やコネクションプール、ロガーなど業務コードの中にも少なくありません。Singleton パターンはその要求に応えるもっとも基本的な設計ですが、正しく実装しないとマルチスレッド環境で複数インスタンスが生まれたり、シリアライズで別オブジェクトが復元されたりといった問題が起きます。この記事では、Eager Initialization・Initialization-on-demand Holder・Enum Singleton の3方式を取り上げ、それぞれのスレッド安全性・遅延初期化・シリアライズ耐性を比較します。double-checked locking が必要になるケースとその落とし穴、Java 標準ライブラリでの Singleton 実例(Runtime.getRuntime)も確認し、実務で迷わない選択基準を示します。

使いどころ

アプリケーション設定を1つのインスタンスに集約し、どのクラスからも同じ値を参照できるようにする

DB コネクションプールの管理クラスを Singleton にして、接続の生成・破棄を一元管理する

ログ出力を統一するロガーを Singleton で提供し、出力先の切り替えを一箇所で制御する

コード例

SingletonPatternSample.java
public class SingletonPatternSample {

    // Holder パターン: 初回アクセス時に JVM が安全に初期化
    static class AppConfig {
        private final String dbUrl;
        private final int maxPool;

        private AppConfig() {
            this.dbUrl = "jdbc:mysql://localhost:3306/app";
            this.maxPool = 10;
        }

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

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

        public String getDbUrl() { return dbUrl; }
        public int getMaxPool() { return maxPool; }
    }

    // Enum Singleton: 最もシンプルかつ安全
    enum Logger {
        INSTANCE;

        public void info(String msg) {
            System.out.println("[INFO] " + msg);
        }

        public void error(String msg) {
            System.out.println("[ERROR] " + msg);
        }
    }

    public static void main(String[] args) {
        // Holder パターン
        var config1 = AppConfig.getInstance();
        var config2 = AppConfig.getInstance();
        System.out.println("同一インスタンス: " + (config1 == config2));
        System.out.println("DB URL: " + config1.getDbUrl());

        // Enum Singleton
        Logger.INSTANCE.info("アプリ起動");
        Logger.INSTANCE.error("接続タイムアウト");

        // Java 標準ライブラリの Singleton 例
        var rt = Runtime.getRuntime();
        System.out.println("最大メモリ: " + rt.maxMemory() / 1024 / 1024 + " MB");
    }
}

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

Version Coverage

var による型推論で呼び出しコードが簡潔になる。record と組み合わせた設定値の保持も選択肢に入る。

Java 17
// Java 17: var で型推論
var s1 = EagerSingleton.getInstance();
var s2 = EagerSingleton.getInstance();
System.out.println(s1 == s2); // true

// Enum Singleton
EnumSingleton.INSTANCE.increment();

Library Comparison

標準 API(Enum Singleton / Holder)依存なしでスレッド安全かつシリアライズ耐性のある Singleton を実装したいとき。DI コンテナを使うプロジェクトでは、フレームワーク側でスコープ管理するほうが自然な場合がある。
Spring(@Component + @Scope)DI コンテナでライフサイクルを管理し、テスト時にモックへ差し替えたいとき。Spring への依存が前提になるため、ライブラリ層やユーティリティでは使いにくい。
Google Guice(@Singleton)軽量な DI コンテナで Singleton スコープを宣言的に管理したいとき。Guice の導入自体が必要になる。小規模プロジェクトではオーバースペックになりやすい。

注意点

Lazy Initialization を synchronized なしで書くと、マルチスレッドで2つ以上のインスタンスが生まれる。テスト環境では再現しにくいため本番で初めて発覚しやすい

Double-Checked Locking では volatile 修飾子が必須。付け忘れると命令の並び替えによって初期化途中のオブジェクトが返る可能性がある

Serializable を implements した Singleton は、デシリアライズ時に新しいインスタンスが生まれる。readResolve() で INSTANCE を返すか、Enum Singleton を使えば回避できる

リフレクションで private コンストラクタを呼び出されると Singleton が壊れる。Enum Singleton はこの攻撃にも耐性がある

FAQ

Enum Singleton と Holder パターンのどちらを使うべきですか。

Effective Java(Item 3)でも推奨されている通り、一般に Enum Singleton が最も安全とされています。シリアライズ・リフレクション攻撃にも耐性があります。ただし継承が必要な場合は Holder パターンを選んでください。

Singleton はテストしにくいと聞きますが対策はありますか。

Singleton 自体にインターフェースを切り、テスト時はモック実装に差し替える方法が一般的です。DI コンテナを使えばフレームワーク側で切り替えられます。

Spring の Bean はデフォルトで Singleton ですが、自前の Singleton は不要ですか。

Spring 管理下のクラスなら @Component で十分です。ただし、DI コンテナ外で動くユーティリティやライブラリ層では、自前の Singleton が依然有用です。

関連書籍

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

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