Head Firstデザインパターンブックから、ダブルチェックロック付きシングルトンパターンが以下のように実装されました。
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
volatile
が使用されている理由がわかりません。 volatile
の使用は、ダブルチェックロック、つまりパフォーマンスを使用する目的に反しませんか?
volatile
が必要な理由を理解するための優れたリソースは、 [〜#〜] jcip [〜#〜] 本にあります。ウィキペディアには、その資料の まともな説明 もあります。
本当の問題は、Thread A
は、instance
の構築が完了する前にinstance
にメモリ空間を割り当てる場合があります。 Thread B
はその割り当てを確認し、使用を試みます。これにより、Thread B
は、instance
の部分的に構築されたバージョンを使用しているために失敗します。
@irreputableが引用しているように、volatileは高価ではありません。たとえそれが高価であったとしても、一貫性はパフォーマンスよりも優先されるべきです。
レイジーシングルトンには、もう1つのクリーンでエレガントな方法があります。
public final class Singleton {
private Singleton() {}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
}
ソース記事: Initialization-on-demand_holder_idiom ウィキペディアから
ソフトウェアエンジニアリングでは、Initialization on Demand Holder(デザインパターン)イディオムは遅延ロードされたシングルトンです。 Javaのすべてのバージョンで、イディオムにより、安全で高度に並行した遅延パフォーマンスが良好な初期化が可能になります。
クラスには初期化するstatic
変数がないため、初期化は簡単に完了します。
静的クラス定義LazyHolder
は、JVMがLazyHolderを実行する必要があると判断するまで初期化されません。
静的クラスLazyHolder
が実行されるのは、静的メソッドgetInstance
がクラスSingletonで呼び出されたときのみです。これが初めて起こると、JVMはLazyHolder
クラスをロードして初期化します。
このソリューションは、特別な言語構造(つまり、volatile
またはsynchronized
)を必要とせずにスレッドセーフです。
まあ、パフォーマンスのための二重チェックのロックはありません。 壊れたパターンです。
感情を脇に置いて、volatile
はここにあります。2番目のスレッドが_instance == null
_を渡すまでに、最初のスレッドがnew Singleton()
をまだ構築していない可能性があるためです。 happens-before実際にオブジェクトを作成しているスレッド以外のスレッドのinstance
への割り当て。
volatile
は、順番にhappens-before読み取りと書き込みの関係を確立し、壊れたパターンを修正します。
パフォーマンスを探している場合は、代わりにホルダー内部静的クラスを使用してください。
持っていない場合、最初のスレッドがnullに設定した後、2番目のスレッドが同期ブロックに入る可能性があり、ローカルキャッシュはそれがnullであると判断します。
1つ目は、正確さのためではなく(もしあなたが正しいなら、それは自己破産するでしょう)、むしろ最適化のためです。
揮発性の読み取りは、それ自体はそれほど高価ではありません。
短いループでgetInstance()
を呼び出すテストを設計して、揮発性読み取りの影響を観察できます。ただし、そのテストは現実的ではありません。そのような状況では、プログラマは通常getInstance()
を1回呼び出し、使用中はインスタンスをキャッシュします。
別の実装は、final
フィールドを使用することです(ウィキペディアを参照)。これには追加の読み取りが必要であり、volatile
バージョンよりも高価になる可能性があります。 final
バージョンはタイトループではより高速になる可能性がありますが、そのテストは前述のように意味がありません。
変数をvolatile
として宣言すると、その変数へのすべてのアクセスがメモリから現在の値を実際に読み取ることが保証されます。
volatile
がないと、コンパイラはメモリアクセスを最適化してレジスタに値を保持するため、変数を最初に使用するときのみ、変数を保持する実際のメモリ位置を読み取ります。これは、最初のアクセスと2番目のアクセスの間に別のスレッドによって変数が変更された場合に問題になります。最初のスレッドには最初の(変更前の)値のコピーのみがあるため、2番目のif
ステートメントは変数の値の古いコピーをテストします。