web-dev-qa-db-ja.com

揮発性のないダブルチェックロック

私は読んだ この質問 ダブルチェックロックを行う方法について:

// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;
FieldType getField() {
    FieldType result = field;
    if (result == null) { // First check (no locking)
        synchronized(this) {
            result = field;
            if (result == null) // Second check (with locking)
                field = result = computeFieldValue();
        }
    }
    return result;
}

私の目的は、volatile属性なしでフィールド(シングルトンではない)の遅延読み込みを機能させることです。フィールドオブジェクトは、初期化後に変更されることはありません。

私の最終的なアプローチをいくつかテストした後:

    private FieldType field;

    FieldType getField() {
        if (field == null) {
            synchronized(this) {
                if (field == null)
                    field = Publisher.publish(computeFieldValue());
            }
        }
        return fieldHolder.field;
    }



public class Publisher {

    public static <T> T publish(T val){
        return new Publish<T>(val).get();
    }

    private static class Publish<T>{
        private final T val;

        public Publish(T val) {
            this.val = val;
        }

        public T get(){
            return val;
        }
    }
}

利点は、揮発性を必要としないためアクセス時間を高速化できる一方で、再利用可能なPublisherクラスを使用してシンプルさを維持することです。


Jcstressを使用してこれをテストしました。 SafeDCLFinalは期待どおりに機能しましたが、UnsafeDCLFinalは一貫性がありませんでした(期待どおり)。この時点で、99%確実に機能しますが、間違っていることを証明してください。 mvn clean install -pl tests-custom -amでコンパイルされ、Java -XX:-UseCompressedOops -jar tests-custom/target/jcstress.jar -t DCLFinalで実行されます。以下のテストコード(主に変更されたシングルトンテストクラス):

/*
 * SafeDCLFinal.Java:
 */

package org.openjdk.jcstress.tests.singletons;

public class SafeDCLFinal {

    @JCStressTest
    @JCStressMeta(GradingSafe.class)
    public static class Unsafe {
        @Actor
        public final void actor1(SafeDCLFinalFactory s) {
            s.getInstance(SingletonUnsafe::new);
        }

        @Actor
        public final void actor2(SafeDCLFinalFactory s, IntResult1 r) {
            r.r1 = Singleton.map(s.getInstance(SingletonUnsafe::new));
        }
    }

    @JCStressTest
    @JCStressMeta(GradingSafe.class)
    public static class Safe {
        @Actor
        public final void actor1(SafeDCLFinalFactory s) {
            s.getInstance(SingletonSafe::new);
        }

        @Actor
        public final void actor2(SafeDCLFinalFactory s, IntResult1 r) {
            r.r1 = Singleton.map(s.getInstance(SingletonSafe::new));
        }
    }


    @State
    public static class SafeDCLFinalFactory {
        private Singleton instance; // specifically non-volatile

        public Singleton getInstance(Supplier<Singleton> s) {
            if (instance == null) {
                synchronized (this) {
                    if (instance == null) {
//                      instance = s.get();
                        instance = Publisher.publish(s.get(), true);
                    }
                }
            }
            return instance;
        }
    }
}

/*
 * UnsafeDCLFinal.Java:
 */

package org.openjdk.jcstress.tests.singletons;

public class UnsafeDCLFinal {

    @JCStressTest
    @JCStressMeta(GradingUnsafe.class)
    public static class Unsafe {
        @Actor
        public final void actor1(UnsafeDCLFinalFactory s) {
            s.getInstance(SingletonUnsafe::new);
        }

        @Actor
        public final void actor2(UnsafeDCLFinalFactory s, IntResult1 r) {
            r.r1 = Singleton.map(s.getInstance(SingletonUnsafe::new));
        }
    }

    @JCStressTest
    @JCStressMeta(GradingUnsafe.class)
    public static class Safe {
        @Actor
        public final void actor1(UnsafeDCLFinalFactory s) {
            s.getInstance(SingletonSafe::new);
        }

        @Actor
        public final void actor2(UnsafeDCLFinalFactory s, IntResult1 r) {
            r.r1 = Singleton.map(s.getInstance(SingletonSafe::new));
        }
    }

    @State
    public static class UnsafeDCLFinalFactory {
        private Singleton instance; // specifically non-volatile

        public Singleton getInstance(Supplier<Singleton> s) {
            if (instance == null) {
                synchronized (this) {
                    if (instance == null) {
//                      instance = s.get();
                        instance = Publisher.publish(s.get(), false);
                    }
                }
            }
            return instance;
        }
    }
}

/*
 * Publisher.Java:
 */

package org.openjdk.jcstress.tests.singletons;

public class Publisher {

    public static <T> T publish(T val, boolean safe){
        if(safe){
            return new SafePublish<T>(val).get();
        }
        return new UnsafePublish<T>(val).get();
    }

    private static class UnsafePublish<T>{
        T val;

        public UnsafePublish(T val) {
            this.val = val;
        }

        public T get(){
            return val;
        }
    }

    private static class SafePublish<T>{
        final T val;

        public SafePublish(T val) {
            this.val = val;
        }

        public T get(){
            return val;
        }
    }
}

Java 8でテスト済みですが、少なくともJava 6+で動作するはずです。 ドキュメントを参照


しかし、これはうまくいくのだろうか:

    // Double-check idiom for lazy initialization of instance fields without volatile
    private FieldHolder fieldHolder = null;
    private static class FieldHolder{
        public final FieldType field;
        FieldHolder(){
            field = computeFieldValue();
        }
    }

    FieldType getField() {
        if (fieldHolder == null) { // First check (no locking)
            synchronized(this) {
                if (fieldHolder == null) // Second check (with locking)
                    fieldHolder = new FieldHolder();
            }
        }
        return fieldHolder.field;
    }

または多分:

    // Double-check idiom for lazy initialization of instance fields without volatile
    private FieldType field = null;
    private static class FieldHolder{
        public final FieldType field;

        FieldHolder(){
            field = computeFieldValue();
        }
    }

    FieldType getField() {
        if (field == null) { // First check (no locking)
            synchronized(this) {
                if (field == null) // Second check (with locking)
                    field = new FieldHolder().field;
            }
        }
        return field;
    }

または:

    // Double-check idiom for lazy initialization of instance fields without volatile
    private FieldType field = null;

    FieldType getField() {
        if (field == null) { // First check (no locking)
            synchronized(this) {
                if (field == null) // Second check (with locking)
                    field = new Object(){
                        public final FieldType field = computeFieldValue();
                    }.field;
            }
        }
        return field;
    }

私はこれが このOracleドキュメント に基づいて機能すると信じています:

最終フィールドの使用モデルは単純なものです。オブジェクトのコンストラクターでオブジェクトの最終フィールドを設定します。また、オブジェクトのコンストラクターが完了する前に別のスレッドが参照できる場所に、構築中のオブジェクトへの参照を記述しないでください。これが続く場合、オブジェクトが別のスレッドに表示されると、そのスレッドは常にそのオブジェクトの最終フィールドの正しく構築されたバージョンを表示します。また、少なくとも最終フィールドと同じくらい最新のそれらの最終フィールドによって参照されるオブジェクトまたは配列のバージョンも表示されます。

27
Kicsi

まず最初に、あなたがしようとしていることはせいぜい危険です。決勝戦でカンニングをしようとすると少し緊張します。 Java言語は、スレッド間の一貫性を処理するための頼りになるツールとしてvolatileを提供します。それを使用してください。

とにかく、関連するアプローチは "Javaでの安全な公開と初期化" で説明されています:

public class FinalWrapperFactory {
  private FinalWrapper wrapper;

  public Singleton get() {
    FinalWrapper w = wrapper;
    if (w == null) { // check 1
      synchronized(this) {
        w = wrapper;
        if (w == null) { // check2
          w = new FinalWrapper(new Singleton());
          wrapper = w;
        }
      }
    }
    return w.instance;
  }

  private static class FinalWrapper {
    public final Singleton instance;
    public FinalWrapper(Singleton instance) {
      this.instance = instance;
    }
  }
}

それは素人の言葉です、それはこのように機能します。 synchronizedは、wrapperをnullとして観察すると適切な同期を生成します。つまり、最初のチェックを完全に削除し、synchronizedをメソッド本体全体。 finalFinalWrapperは、null以外のwrapperが見つかった場合に保証され、完全に構築され、すべてのSingletonフィールドが表示されます-これは、 wrapperのきちんとした読み取り。

値自体ではなく、フィールドのFinalWrapperを引き継ぐことに注意してください。 instanceFinalWrapperなしで公開された場合、すべての賭けは無効になります(素人の言葉で言えば、それは時期尚早の公開です)。これがPublisher.publishは機能しません。最後のフィールドに値を入力して読み戻し、安全でない方法で公開するだけでは安全ではありません。これは、裸のinstanceを書き出すのと非常によく似ています。

また、null wrapperを検出し、その値を使用する場合は、ロックの下で「フォールバック」を読み取るように注意する必要があります。 -)。 returnステートメントでwrapperの2回目(3回目)の読み取りを行うと、正当性が損なわれ、正当なレースに向けてセットアップされます。

編集:ちなみに、公開しているオブジェクトが内部でfinal- sで覆われている場合は、FinalWrapperの仲介者を切り取って、instance自体。

編集2: LCK10-J。ダブルチェックされたロックイディオムの正しい形式を使用する 、およびそこにあるコメントでのいくつかの議論も参照してください。

28

要するに

volatileまたはラッパークラスのないコードのバージョンは、JVMが実行されている基盤となるオペレーティングシステムのメモリモデルに依存します。

ラッパークラスを含むバージョンは、 Initialization on Demand Holder デザインパターンとして知られる既知の代替手段であり、最初のアクセス時に任意のクラスが最大1回ロードされるClassLoaderコントラクトに依存します。 、そしてスレッドセーフな方法で。

volatileの必要性

開発者がコード実行について考える方法のほとんどは、プログラムがメインメモリにロードされ、そこから直接実行されるというものです。ただし、実際には、メインメモリとプロセッサコアの間に多数のハードウェアキャッシュが存在します。この問題は、各スレッドが別々のプロセッサで実行され、それぞれがスコープ内の変数の独自の独立したコピーを持つために発生します。論理的にはfieldを1つの場所として考えるのが好きですが、現実はより複雑です。

単純な(おそらく冗長ですが)例を実行するために、2つのスレッドと単一レベルのハードウェアキャッシュがあり、各スレッドがそのキャッシュにfieldの独自のコピーを持っているシナリオを考えます。したがって、すでにfieldの3つのバージョンがあります。1つはメインメモリに、1つは最初のコピーに、もう1つは2番目のコピーにあります。これらをfieldと呼びますMfield、およびfieldB それぞれ。

  1. 初期状態
    fieldM = null
    field = null
    fieldB = null
  2. スレッドAは最初のnullチェックを実行し、fieldを見つけます 無効です。
  3. スレッドAはthisのロックを取得します。
  4. スレッドBは最初のnullチェックを実行し、fieldを見つけますB 無効です。
  5. スレッドBはthisのロックを取得しようとしますが、スレッドAによって保持されていることを検出します。スレッドBはスリープします。
  6. スレッドAは2番目のヌルチェックを実行し、fieldを見つけます 無効です。
  7. スレッドAはfieldを割り当てますfieldType1およびロックを解放します。 fieldvolatileではないため、この割り当ては伝達されません。
    fieldM = null
    field = fieldType1
    fieldB = null
  8. スレッドBが呼び起こされ、thisのロックを取得します。
  9. スレッドBは2番目のnullチェックを実行し、fieldを見つけますB 無効です。
  10. スレッドBはfieldを割り当てますBfieldType2で、ロックを解除します。
    fieldM = null
    field = fieldType1
    fieldB = fieldType2
  11. ある時点で、キャッシュコピーAへの書き込みはメインメモリに同期されます。
    fieldM = fieldType1
    field = fieldType1
    fieldB = fieldType2
  12. 後の時点で、キャッシュコピーBへの書き込みが同期されてメインメモリに同期され、コピーAによって行われた割り当てが上書きされます
    fieldM = fieldType2
    field = fieldType1
    fieldB = fieldType2

上記の質問に対するコメント投稿者の1人として、volatileを使用すると、書き込みが確実に表示されます。これを保証するために使用されるメカニズムがわかりません。変更が各コピーに伝播される可能性があります。そもそもコピーが作成されることはなく、fieldへのすべてのアクセスが反対である可能性があります。メインメモリ。

これに関する最後の注意:結果はシステムに依存することを前述しました。これは、基盤となるさまざまなシステムがメモリモデルに対して楽観的でないアプローチを取り、スレッド間で共有されるallメモリをvolatileとして扱うか、ヒューリスティックを適用して特定の参照はvolatileとして扱われるかどうかに関係なく、メインメモリへの同期のパフォーマンスが犠牲になります。これにより、これらの問題のテストが悪夢になる可能性があります。競合状態をトリガーするのに十分な大きさのサンプルに対して実行する必要があるだけでなく、条件をトリガーしないように十分に保守的なシステムでテストしている場合があります。

初期化オンデマンドホルダー

ここで指摘したい主なことは、本質的にシングルトンをミックスにこっそり入れているため、これが機能することです。 ClassLoaderコントラクトは、Classのインスタンスが多数存在する可能性がある一方で、どのタイプのAでも使用できるClass<A>のインスタンスは1つだけであることを意味します。最初の参照/遅延初期化時に最初にロードされます。実際、クラスの定義の静的フィールドは、そのクラスに関連付けられたシングルトンのフィールドであり、そのシングルトンとクラスのインスタンスの間でメンバーアクセス権限が増加していると考えることができます。

6
hayden.sikh

引用 「ダブルチェックロックが壊れている」宣言 @Kicsiが言及しました。最後のセクションは次のとおりです。

不変オブジェクトのロックの二重チェック

Helperが不変オブジェクトで、Helperのすべてのフィールドがfinalである場合、ダブルチェックロックは、揮発性フィールドを使用しなくても機能します。不変オブジェクト(文字列や整数など)への参照は、intまたはfloatとほぼ同じように動作する必要があるという考え方です。不変オブジェクトへの参照の読み取りと書き込みはアトミックです。

(強調は私のものです)

FieldHolderは不変であるため、実際にはvolatileキーワードは必要ありません。他のスレッドは常に適切に初期化されたFieldHolderを参照します。したがって、私が理解している限り、FieldTypeは、他のスレッドからFieldHolderを介してアクセスできるようになる前に、常に初期化されます。

ただし、FieldTypeが不変でない場合は、適切な同期が必要です。結果として、volatileキーワードを回避することで多くのメリットが得られるかどうかはわかりません。

ただし、不変であれば、上記の引用に従って、FieldHolderはまったく必要ありません。

2
Didier L

遅延初期化にEnumまたはネストされた静的クラスヘルパーを使用する場合は、静的を使用する初期化にそれほどコスト(スペースまたは時間)がかからない場合は、初期化。

public enum EnumSingleton {
    /**
     * using enum indeed avoid reflection intruding but also limit the ability of the instance;
     */
    INSTANCE;

    SingletonTypeEnum getType() {
        return SingletonTypeEnum.ENUM;
    }
}

/**
 * Singleton:
 * The JLS guarantees that a class is only loaded when it's used for the first time
 * (making the singleton initialization lazy)
 *
 * Thread-safe:
 * class loading is thread-safe (making the getInstance() method thread-safe as well)
 *
 */
private static class SingletonHelper {
    private static final LazyInitializedSingleton INSTANCE = new LazyInitializedSingleton();
}

「ダブルチェックロックが壊れている」宣言

この変更により、ヘルパーフィールドを揮発性として宣言することにより、ダブルチェックロックイディオムを機能させることができます。これはJDK4以前では機能しません。

  class Foo {
        private volatile Helper helper = null;
        public Helper getHelper() {
            if (helper == null) {
                synchronized(this) {
                    if (helper == null)
                        helper = new Helper();
                }
            }
            return helper;
        }
    }
0
Hearen