web-dev-qa-db-ja.com

ダブルチェックロックで揮発性が使用される理由

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の使用は、ダブルチェックロック、つまりパフォーマンスを使用する目的に反しませんか?

65
toc777

volatileが必要な理由を理解するための優れたリソースは、 [〜#〜] jcip [〜#〜] 本にあります。ウィキペディアには、その資料の まともな説明 もあります。

本当の問題は、Thread Aは、instanceの構築が完了する前にinstanceにメモリ空間を割り当てる場合があります。 Thread Bはその割り当てを確認し、使用を試みます。これにより、Thread Bは、instanceの部分的に構築されたバージョンを使用しているために失敗します。

59
Tim Bender

@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)を必要とせずにスレッドセーフです。

17
Ravindra babu

まあ、パフォーマンスのための二重チェックのロックはありません。 壊れたパターンです。

感情を脇に置いて、volatileはここにあります。2番目のスレッドが_instance == null_を渡すまでに、最初のスレッドがnew Singleton()をまだ構築していない可能性があるためです。 happens-before実際にオブジェクトを作成しているスレッド以外のスレッドのinstanceへの割り当て。

volatileは、順番にhappens-before読み取りと書き込みの関係を確立し、壊れたパターンを修正します。

パフォーマンスを探している場合は、代わりにホルダー内部静的クラスを使用してください。

9
alf

持っていない場合、最初のスレッドがnullに設定した後、2番目のスレッドが同期ブロックに入る可能性があり、ローカルキャッシュはそれがnullであると判断します。

1つ目は、正確さのためではなく(もしあなたが正しいなら、それは自己破産するでしょう)、むしろ最適化のためです。

2
corsiKa

揮発性の読み取りは、それ自体はそれほど高価ではありません。

短いループでgetInstance()を呼び出すテストを設計して、揮発性読み取りの影響を観察できます。ただし、そのテストは現実的ではありません。そのような状況では、プログラマは通常getInstance()を1回呼び出し、使用中はインスタンスをキャッシュします。

別の実装は、finalフィールドを使用することです(ウィキペディアを参照)。これには追加の読み取りが必要であり、volatileバージョンよりも高価になる可能性があります。 finalバージョンはタイトループではより高速になる可能性がありますが、そのテストは前述のように意味がありません。

1
irreputable

変数をvolatileとして宣言すると、その変数へのすべてのアクセスがメモリから現在の値を実際に読み取ることが保証されます。

volatileがないと、コンパイラはメモリアクセスを最適化してレジスタに値を保持するため、変数を最初に使用するときのみ、変数を保持する実際のメモリ位置を読み取ります。これは、最初のアクセスと2番目のアクセスの間に別のスレッドによって変数が変更された場合に問題になります。最初のスレッドには最初の(変更前の)値のコピーのみがあるため、2番目のifステートメントは変数の値の古いコピーをテストします。

0
David R Tribble