概要
アプリケーション全体でインスタンスを1つだけ保持したい場面は、設定管理やコネクションプール、ロガーなど業務コードの中にも少なくありません。Singleton パターンはその要求に応えるもっとも基本的な設計ですが、正しく実装しないとマルチスレッド環境で複数インスタンスが生まれたり、シリアライズで別オブジェクトが復元されたりといった問題が起きます。この記事では、Eager Initialization・Initialization-on-demand Holder・Enum Singleton の3方式を取り上げ、それぞれのスレッド安全性・遅延初期化・シリアライズ耐性を比較します。double-checked locking が必要になるケースとその落とし穴、Java 標準ライブラリでの Singleton 実例(Runtime.getRuntime)も確認し、実務で迷わない選択基準を示します。
使いどころ
アプリケーション設定を1つのインスタンスに集約し、どのクラスからも同じ値を参照できるようにする
DB コネクションプールの管理クラスを Singleton にして、接続の生成・破棄を一元管理する
ログ出力を統一するロガーを Singleton で提供し、出力先の切り替えを一箇所で制御する
コード例
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");
}
}Version Coverage
var による型推論で呼び出しコードが簡潔になる。record と組み合わせた設定値の保持も選択肢に入る。
// Java 17: var で型推論 var s1 = EagerSingleton.getInstance(); var s2 = EagerSingleton.getInstance(); System.out.println(s1 == s2); // true // Enum Singleton EnumSingleton.INSTANCE.increment();
Library Comparison
注意点
Lazy Initialization を synchronized なしで書くと、マルチスレッドで2つ以上のインスタンスが生まれる。テスト環境では再現しにくいため本番で初めて発覚しやすい
Double-Checked Locking では volatile 修飾子が必須。付け忘れると命令の並び替えによって初期化途中のオブジェクトが返る可能性がある
Serializable を implements した Singleton は、デシリアライズ時に新しいインスタンスが生まれる。readResolve() で INSTANCE を返すか、Enum Singleton を使えば回避できる
リフレクションで private コンストラクタを呼び出されると Singleton が壊れる。Enum Singleton はこの攻撃にも耐性がある
FAQ
Effective Java(Item 3)でも推奨されている通り、一般に Enum Singleton が最も安全とされています。シリアライズ・リフレクション攻撃にも耐性があります。ただし継承が必要な場合は Holder パターンを選んでください。
Singleton 自体にインターフェースを切り、テスト時はモック実装に差し替える方法が一般的です。DI コンテナを使えばフレームワーク側で切り替えられます。
Spring 管理下のクラスなら @Component で十分です。ただし、DI コンテナ外で動くユーティリティやライブラリ層では、自前の Singleton が依然有用です。