Singleton パターン — 2020 年代の Java では `enum` 一択の理由
伝統的な Singleton の 4 実装と、各々のスレッド安全性・シリアライズ耐性・リフレクション耐性。結論として現代は enum 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 を書く前に、「グローバル状態を増やすことに本当に価値があるか」を一度疑う
関連記事
- アルゴリズム·
TemplateMethod パターン — Spring JdbcTemplate に見る典型実装
処理の骨格は親、差分は子に書き分ける Template Method。Spring の `*Template` 系の設計思想と、Strategy / Decorator との使い分け。
- アルゴリズム·
Adapter パターン — 外部ライブラリをドメインに染めない防波堤
既存の API を自分たちのドメインに合わせて包むアダプタ。クラスアダプタ vs オブジェクトアダプタ、SLF4J や Spring の HandlerAdapter などの実例まで。
- アルゴリズム·
Iterator パターン — Java では `Iterable` 実装 + 拡張 for で終わり
GoF の Iterator パターンを Java で書くときの最小実装。拡張 for 文、Spliterator、Stream との関係まで整理する。