DP-05: Singleton パターン
アプリケーション全体でインスタンスを1つに制限するパターンです。java.lang.Runtime など 標準ライブラリでも使われています。実装方法は複数あり、それぞれの特徴を理解して使い分けることが大切です。
Singleton パターンとは
設定情報・ログ出力・データベース接続プールなど、アプリケーション全体で1つだけ存在すべきオブジェクトに使います。 コンストラクタを private にして外部からの生成を禁止し、getInstance() メソッドで唯一のインスタンスを返します。
| パターン | 遅延初期化 | スレッドセーフ | シンプルさ |
|---|---|---|---|
| Eager Initialization | ❌ | ✅ | ◎ |
| Lazy(スレッドアンセーフ) | ✅ | ❌ | △(使用非推奨) |
| Double-Checked Locking | ✅ | ✅(volatile 必須) | △ |
| Holder(推奨) | ✅ | ✅ | ◎ |
| Enum Singleton | ❌ | ✅ | ◎(最もシンプル) |
通常は Holder パターン か Enum Singleton を使いましょう。 Holder パターンは遅延初期化(初めて使われたときに生成)とスレッドセーフを同時に達成できます。 Enum Singleton はさらにシンプルで、直列化(Serializable)の問題も自動的に解決されます。
サンプルコード
Java 8 では Holder パターン(Initialization-on-demand holder)が推奨です。volatile や synchronized を使わずに遅延初期化とスレッドセーフを両立できます。
よくあるミス・注意点
⚠️ Lazy + スレッドアンセーフな実装(if == null チェックのみ)
if (instance == null) { instance = new ... } だけでは、 複数のスレッドが同時に null チェックを通過してしまい、 2つ以上のインスタンスが生成される可能性があります。 スレッドセーフが必要な場合は Holder パターンか Enum Singleton を使いましょう。
⚠️ Double-Checked Locking で volatile を忘れる
volatile を付けないと、 CPU の命令並べ替え最適化(リオーダリング)によって初期化が完了していないインスタンスが 別スレッドから参照されることがあります。 Double-Checked Locking を使う場合は private static volatile が必須です。
⚠️ Singleton にグローバルな可変状態を持たせすぎる
Singleton は便利な反面、テスト時にモック(偽物のオブジェクト)への差し替えが難しくなります。 また、スレッドセーフを保つために同期化のコストも発生します。 本当に1つである必要があるかどうかを設計時に確認しましょう。
テストする観点
getInstance()を複数回呼んでも同一インスタンス(== が true)が返ること- 複数スレッドから同時に
getInstance()を呼んでも同一インスタンスが返ること(スレッドセーフ) - 1つのスレッドでインクリメントした値を別のスレッドから取得すると同じ値になること(状態の共有)
- Enum Singleton はシリアライズ・デシリアライズ後も同一インスタンスであること(境界値)
- コンストラクタが
privateであるため、リフレクションを除いて外部から直接生成できないこと