Singleton パターン
Singleton パターン(シングルトン・パターン)とは、オブジェクト指向のコンピュータプログラムにおける、デザインパターンの1つである。GoF (Gang of Four; 4人のギャングたち) によって定義された。Singleton パターンとは、そのクラスのインスタンスが1つしか生成されないことを保証するデザインパターンのことである。ロケールやルック・アンド・フィールなど、絶対にアプリケーション全体で統一しなければならない仕組みの実装に使用される[1]。
クラス図
編集Singleton パターンの一般的なクラス図を示す。
このクラス図で注目すべきことは以下の3点である。
- 同じ型のインスタンスが private なクラス変数として定義されている。
- コンストラクタの可視性が private である。
- 同じ型のインスタンスを返す
getInstance()
がクラス関数として定義されている。
クラス図内にあるアンダーラインは、その項目がクラス変数あるいはクラス関数であることを意味している。
Javaでの実装例
編集以下にSingleton パターンを用いたクラスのJavaによる例を示す。
final class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
このクラスにおいて、コンストラクタはprivate
で定義されているため、他のクラスによって、Singleton
クラスのインスタンスを生成することはできない。このクラスのインスタンスを生成したいときは、getInstance()
メソッドを利用することになるが、このメソッドは最初に呼び出されたときにだけインスタンスを生成し、2回目以降に呼び出されたときは最初に生成したインスタンスを返すように作られている。そのため、プログラム中にSingleton
クラスのインスタンスが1つしか存在しないことが保証される。
getInstance()
メソッドがsynchronized
に指定されているのは、複数のスレッドからほぼ同時に呼び出された際に複数のインスタンスが生成されてしまう危険性をなくすためである。
問題点および改善策
編集Java における一般的な Singleton パターンの記述は上述した通りであるが、この方法は同期化コストが高い。そこでdouble-checked lockingというイディオム[2]が考えられたが、「アウトオブオーダー書き込み」を許すJavaプラットフォームのメモリーモデルが原因で、同期化に失敗する(言うまでもないが、この最適化による副作用は Java だけのものではない)。この問題を克服しようとするとコードが肥大化しコストがかかる。そこで現在では以下のように static フィールドを用いることが推奨されている。
final class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return Singleton.instance;
}
}
これによりコストは改善される。同期化は行われないが、static フィールドの初期化はそのクラスが呼び出される最初の一回しか行われないため、何回getInstance()
メソッドを呼んでもスレッドアンセーフを心配する必要はなくなるだけでなく、コストパフォーマンスも非常に高い。
ただしこの場合、Singletonクラスがロードされたときに初期化されるのであって、getInstance()
が初めて呼ばれたときではない。このことはプログラマーの意図しないタイミングで初期化が始まってしまい混乱の元となる場合がある。
そこで en:Initialization-on-demand holder idiom と呼ばれる手法、すなわちinstanceフィールドのみを別のホルダークラスに隔離して、そのホルダークラスがロードされたときにSingletonの初期化が行なわれるよう改善したものが下記である。
final class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
これでSingletonクラスがロードされたときではなく、getInstance()
呼び出しによりSingletonHolderクラスがロードされたときにSingletonクラスが初期化される。
環境ごとの注意点
編集上記のSingletonは、Javaクラスのアンロードを考慮していない。Javaでは、参照されなくなったクラスはガベージコレクションにより回収され、Java仮想マシン (JVM) からアンロードされることがありえる。クラスのアンロードにより、そのクラスのstaticフィールドもまた無効となる。つまり、staticフィールドの寿命はアプリケーションの寿命と同一ではなくなる。アンロードされたクラスは再度必要になったときにリロードされ、クラスのstaticイニシャライザも再度呼び出される。こうした一連の動作によりstaticフィールド上のインスタンスは再生成されてしまう。また、各staticフィールドはJVMごとにひとつ存在するのではなく、ロードされたクラスごとにひとつ存在するため、Singletonが破たんするケースもありえる[3]。
AndroidのDalvik/ART環境上では、サスペンドされたアプリケーションのActivityは、メモリが足らなくなったときや長時間放置されたときなどに破棄されることがあるが、その際、Activityコンテキストに属するすべてのandroid.view.Viewが無効となり、これらは通例アプリケーション再開時に呼ばれるandroid.app.Activity.onCreate()で再初期化することになる。しかし、クラス自体がアンロードされることも起こりうる[4][5]。アンロードされたクラスはアプリケーションの再開時に必要に応じてリロードされる。
Android環境下でアプリケーションの寿命と同等のstaticフィールドを使用したい場合、独自のandroid.app.Application派生クラスを定義してAndroidManifest.xmlに記述する。通常はApplicationのサブクラス化は必要なく、たいていのケースでは、(クラスの再初期化が起こりうることに注意してさえいれば)staticシングルトンで同等機能を提供できるとされている[6]。
Androidは通例リソースに制限のあるモバイル環境であることもあいまって、オブジェクトのライフサイクルは比較的短く、アプリケーションのサスペンドによりクラスのアンロードが発生しやすくなる。そもそもAndroidの仮想マシンは正式なJava SE/Java ME仕様に則っていない。しかし、たとえ正式なJava SE/Jakarta EE(旧・Java EE)仕様に則ったデスクトップ環境やサーバー環境であっても、クラスのアンロードは起こりうる[7][8][9]。
このように、SingletonパターンはJavaという言語だけで考えてはならず、実行環境によってもあり方が変わってくるので注意が必要である。Androidのようにフレームワーク内でSingletonを行いたい場合はフレームワーク提供の機構を使うことを検討しなくてはならない。Java EE 6では@Singleton
アノテーションが導入されている。
PHP 5.x以降での実装例
編集バージョン5.0以降のPHPでは、可視性、クラス関数、クラス変数などの機能を備えたことで、より Java に近い仕様となったため、#Javaでの実装例と同じ原理で Singleton パターンを実現できる。ソースコードも Java のサンプルコードとほとんど同じものになるため、ここでは省略する。
C++での実装例
編集C++ではクラスのコンストラクタ、コピーコンストラクタとコピー代入演算子を private にすることで唯一となる。さらにデストラクタを private にすることで派生クラスのスタック上への作成を禁止できる。
静的ローカル変数を利用した実装例を示す。
class Singleton {
private:
Singleton() {} // コンストラクタを private に置く。
Singleton(const Singleton&); // コピーコンストラクタも private に置き、定義しない。
Singleton& operator=(const Singleton&); // コピー代入演算子も private に置き、定義しない。
~Singleton() {} // デストラクタを private に置く。
public:
static Singleton& getInstance() {
static Singleton inst; // private なコンストラクタを呼び出す。
return inst;
}
const char* getString() const {
return "Hello world!";
}
};
// 利用例。
int main() {
std::cout << Singleton::getInstance().getString() << std::endl;
}
静的ローカル変数を利用して実装した場合、シングルトンインスタンスを削除するタイミングを明示的に制御できないことに注意が必要となる。また、インスタンスが生成されるのは最初にgetInstance()関数を呼び出したタイミングとなるが、C++11よりも前の規格では静的ローカル変数の初期化はスレッドセーフ性が保証されないため、複数のスレッドから同時に初回アクセスが発生した場合、未定義動作を引き起こす。静的ローカル変数ではなく、ポインタ型の静的メンバー変数を利用して実装する場合でも、double-checked lockingなどの技法によるスレッドセーフ化が必要となる。
なお、C++11規格では、コンパイラが生成する関数へのdefault/delete指定により、コンストラクタ/デストラクタ以外はprivateに置かなくてもよくなった。また、finalキーワードでクラスを修飾することで、派生クラスの定義を禁止することができる。さらに、静的ローカル変数の初期化は自動的に排他制御され、スレッドセーフとなる[10]。
class Singleton final {
private:
Singleton() = default; // コンストラクタを private に置く。
~Singleton() = default; // デストラクタを private に置く。
public:
Singleton(const Singleton&) = delete; // コピーコンストラクタを delete 指定。
Singleton& operator=(const Singleton&) = delete; // コピー代入演算子も delete 指定。
Singleton(Singleton&&) = delete; // ムーブコンストラクタを delete 指定。
Singleton& operator=(Singleton&&) = delete; // ムーブ代入演算子も delete 指定。
...
};
特徴
編集扱い次第では、グローバル変数のように機能させることもできる。例えば Java にグローバル変数はないと言われているが、この Singleton パターンで Singleton クラスを作成することで、コード中のどこからでも同一のインスタンスにアクセスすることができる。これはグローバル変数そのものであり、ゆえに Singleton パターンはグローバル変数と同様の問題を引き起こす危険性をはらんでいる。ただし、パッケージ(名前空間)を指定することにより、インスタンスにアクセス可能なコード範囲を制限し、この問題を回避ないしは軽減することはできる。
また、コードを工夫すればインスタンスの個数制限を設けることもできる。たとえば、上記のサンプルコード中の getInstance() を複数回呼び出したとき、10回目までは毎回新しいインスタンスを生成するが、11回目以降は以前のインスタンスを再利用する、といったような機構である。これは単なるグローバル変数で実現することはできない利点である。
Multiton パターン
編集Singletonパターンを拡張したデザインパターンとしてMultiton パターンと呼ばれるものがある。これはSingletonで生成されるインスタンスを連想配列で複数保持する。ただしMultiton パターンはユニットテストを難しくし[11]、ガーベジコレクションのある言語ではオブジェクトへの強い参照によりメモリリークを起こす恐れがあることも念頭に置く必要がある。
Javaでの例
編集public class FooMultiton {
private static final Map<Object, FooMultiton> instances = new HashMap<Object, FooMultiton>();
private FooMultiton() {
// no explicit implementation
}
public static synchronized FooMultiton getInstance(Object key) {
// Our "per key" singleton
FooMultiton instance = instances.get(key);
if (instance == null) {
// Lazily create instance
instance = new FooMultiton();
// Add it to map
instances.put(key, instance);
}
return instance;
}
// other fields and methods ...
}
脚注
編集- ^ エリック・ガンマ、ラルフ・ジョンソン、リチャード・ヘルム、ジョン・ブリシディース(著)、グラディ・ブーチ(まえがき)、本位田真一、吉田和樹(監訳)、『オブジェクト指向における再利用のためのデザインパターン』、ソフトバンクパブリッシング、1995。ISBN 978-4-7973-1112-9.
- ^ double-checked lockingとSingletonパターン - この破綻したプログラミング・イディオムを多角的に検討する | IBM, Internet Archive
- ^ クラスローダーとJ2EEパッケージング戦略を理解する: 第2回「クラスローダーを理解する - シングルトンがシングルトンでなくなる日」| IBM, Internet Archive
- ^ JNI tips | Android NDK | Android Developers"Classes are only unloaded if all classes associated with a ClassLoader can be garbage collected, which is rare but will not be impossible in Android."
- ^ JNI に関するヒント | Android NDK | Android Developers“クラスがアンロードされるのは、クラスローダーに関連付けられているすべてのクラスに対してガベージ コレクションが可能な場合に限られます。この状況は Android ではまれにしか起こりませんが、あり得ないことではありません。”
- ^ Application | Android Developers"There is normally no need to subclass Application. In most situations, static singletons can provide the same functionality in a more modular way."
- ^ Class Unloading in Layered Java Applications - Gunnar Morling
- ^ Chapter 12. Execution §12.7. Unloading of Classes and Interfaces | Oracle Java SE 15 > Java SE Specifications > Java Language Specification
- ^ Java のクラスアンロード (Class Unloading)
- ^ ブロックスコープを持つstatic変数初期化のスレッドセーフ化 - cpprefjp C++日本語リファレンス
- ^ Google Testing Blog: Clean Code Talks - Global State and Singletons