あるクラスのインスタンスが唯一であることを保証し、そのインスタンスを取得する方法を提供するのがSingletonパターン。
つまり、システムでインスタンスが「ひとつしか存在してはならない場合」、「二つ以上存在しても意味が無い場合(メモリ領域やインスタンス生成のオーバーヘッドがもったいない)」、などがSingletonを適用する場面となる。
Javaで普通にSingletonを実装した場合、システムで唯一のインスタンスにするのは難しいので、VMでひとつ、正確にはクラスローダでロードされたクラス定義ごとにひとつ、となるのが普通。
Singletonパターンの構成はいたってシンプル。
Singletonパターンを適用するクラス(図ではSingletonクラス)に、インスタンスを格納するスタティックメンバを確保する。このメンバに格納するインスタンスが唯一になるよう実装し、スタティックメソッドによってそのインスタンスを取得できるようにしておく。
実は、メソッドも変数もスタティックで宣言してやれば、Singletonパターンと似たようなことが出来る。実際、インスタンスを取得する必要がない分、若干だが使うほうは楽になるという面もある。
ではSingletonパターンのメリットはどうかというと、
といった感じだろうか。3つめなんかはgetInstanceメソッドを上手く定義してやれば非常に柔軟なアプリケーションを構築できる。
念のために言っておくが、この「Double-Checked Locking」は、(Javaでは)使っちゃ駄目なテクニックなのでよろしく。
唯一のインスタンスをいつ生成するのか、どう生成するのかでちょっとした問題がある。
一番シンプルなのはこんな感じだろう。
public class Singleton{
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
private Singleton(){
}
}
インスタンスを保持するメンバの宣言時に代入してしまう。普通はこれで事足りるだろう。
で、ありがちなのは、必要になるまでインスタンスを作らないパターン。
public class Singleton{
private static Singleton instance = null;
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
private Singleton(){
}
}
インスタンスがnullの場合のみあたらしく生成する。これはこれでいいのだが、メソッドにsynchronizedがかかっているので、ちょっと重いわけだ(synchronizedをつけとかないと、複数のスレッドがほぼ同時にifブロックに入る可能性があり、複数のインスタンスが存在することになる可能性があるので「唯一」のインスタンスであることが保障できない)。ってこんなのが気になるようなレベルで処理の重さを考えることなんて普通ないけど。とはいえ同期の範囲は狭いに越したことはないし、できればインスタンスが生成されてからは同期せずにinstanceを返したい。
そこで考えられたのがタイトルに書いたDouble-Checked Locking。
public class Singleton{
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
//ここは複数スレッドが入ってくる可能性がある
synchronized(Singleton.class){
if(instance == null){
//ここは同期が取られているので一度しか実行されない
instance = new Singleton();
}
}
}
return instance;
}
private Singleton(){
}
}
なんだか無駄に見えるが、こうしておけば、最初のifにより、すでにインスタンスが生成されている場合は同期しなくてすむ。
instanceがnullの場合同期ブロックに入るわけだが、最初のifは同期外なので、実は最初のifブロックの中には複数スレッドが入ってくる可能性がある。そのため、同期ブロック内でもう一度、instanceがnullかどうか確かめている。
と、ここまで書いといてなんだが、最初にも書いてある通り、このロジックは安全ではない。
Javaのメモリ管理の関係で、「インスタンスの生成が終わってなくても、instanceがnullではない」という状態がある(らしい)のだ。
スレッド1がnew Singleton()しているとき(でも完了していないとき)に、instanceが「nullではないがインスタンスが正しく生成されてない」という状態になり、そこでスレッド2がgetInstanceを実行すると、instanceがnullではないのでifブロックに入らず、return instanceが実行されてしまうのだ。
とはいっても、手元で実験した限りではそうはならない。どうも、VMの種類やバージョン、あるいはJITコンパイラ等の具合によってはそういうこともある、ということらしい。とはいえ、素直に最初の2つのどっちかでやるのがいいでしょ、というお話。