42

最近、Java の二重チェック ロック パターンとその落とし穴について説明している記事を偶然目にしました。今、私が何年も使用しているそのパターンの変形が問題の影響を受けやすいかどうか疑問に思っています。

この件に関する多くの投稿や記事を見て、部分的に構築されたオブジェクトへの参照を取得する際の潜在的な問題を理解しました。私が知る限り、私の実装はこれらの問題の影響を受けるとは思いません. 次のパターンに問題はありますか?

もしそうでなければ、なぜ人々はそれを使わないのでしょうか? この問題に関して私が見たどの議論でも、それが推奨されているのを見たことがありません。

public class Test {
    private static Test instance;
    private static boolean initialized = false;

    public static Test getInstance() {
        if (!initialized) {
            synchronized (Test.class) {
                if (!initialized) {
                    instance = new Test();
                    initialized = true;
                }
            }
        }
        return instance;
    }
}
4

11 に答える 11

26

ダブルチェックロックが壊れています。初期化はプリミティブであるため、動作するために揮発性である必要はないかもしれませんが、インスタンスが初期化される前に、同期化されていないコードに対して初期化されていると見なされることを妨げるものは何もありません。

編集:上記の答えを明確にするために、元の質問はブール値を使用してダブルチェックロックを制御することについて尋ねました。上記のリンクの解決策がなければ、機能しません。実際にブール値を設定しているロックを再確認することはできますが、クラス インスタンスの作成に関しては、命令の並べ替えに関する問題がまだ残っています。初期化されたブール値が同期化されていないブロックで true として表示された後、インスタンスが初期化されない可能性があるため、提案された解決策は機能しません。

ロックを再確認する適切な解決策は、(インスタンス フィールドで) volatile を使用し、初期化されたブール値を忘れて、必ず JDK 1.5 以降を使用するか、最終フィールドで初期化することです。記事とトムの答え、または単に使用しないでください。

確かに、このシングルトンを取得する際に大量のスレッド競合が発生することがわかっている場合、またはアプリケーションのプロファイルを作成し、これがホット スポットであることがわかっている場合を除き、コンセプト全体が非常に時期尚早な最適化のように思えます。

于 2009-10-26T14:34:08.247 に答える
17

initializedだったらうまくいくでしょうvolatilesynchronizedの興味深い効果と同様にvolatile、他のデータについて言えることほど、参照とはあまり関係がありません。instanceフィールドとオブジェクトの設定は、への書き込みの前に発生Testするように強制されます。ショート サーキットを介してキャッシュされた値を使用する場合、参照を介して および オブジェクトに到達する前に読み取りが行われます。別のフラグを使用しても大きな違いはありません(コードがさらに複雑になることを除けば)。initializedinitializeinstanceinitialized

(安全でないパブリケーションのコンストラクターのフィールドのルールfinalは少し異なります。)

ただし、この場合、バグはめったに見られません。初めて使用するときのトラブルの可能性は最小限で、繰り返しのないレースです。

コードが複雑すぎる。次のように記述できます。

private static final Test instance = new Test();

public static Test getInstance() {
    return instance;
}
于 2009-10-26T14:31:01.950 に答える
5

これが、二重チェック ロックが壊れている理由です。

Synchronize は、1 つのスレッドのみがコード ブロックに入ることができることを保証します。ただし、同期セクション内で行われた変数の変更が他のスレッドに表示されることは保証されません。同期ブロックに入るスレッドだけが、変更を確認できることが保証されます。これが、ダブル チェック ロックが壊れている理由です。リーダー側で同期されていません。読み取りスレッドは、シングルトンが null ではないことを確認できますが、シングルトン データが完全に初期化されていない (表示されていない) 可能性があります。

注文は によって提供されvolatileます。volatileたとえば、揮発性シングルトン静的フィールドへの書き込みは、単一オブジェクトへの書き込みが揮発性静的フィールドへの書き込みの前に終了することを保証します。2 つのオブジェクトのシングルトンの作成を妨げるものではありません。これは同期によって提供されます。

クラス final 静的フィールドは揮発性である必要はありません。Java では、JVMがこの問題を処理します。

私の投稿を参照してください。Singleton patternへの回答と現実世界の Java アプリケーションでの壊れた二重チェック ロックは、巧妙に見えますが壊れている二重チェック ロックに関するシングルトンの例を示しています。

于 2010-08-18T20:46:11.917 に答える
2

二重チェックのロックはアンチパターンです。

Lazy Initialization Holder Classは、注目すべきパターンです。

他の多くの回答にもかかわらず、なぜ DCL が多くのコンテキストで壊れているのか、なぜそれが不要なのか、代わりに何をすべきかを示す簡単な回答がまだ 1 つもないため、回答する必要があると考えました。Goetz: Java Concurrency In Practiceそのため、Java メモリ モデルに関する最終章で最も簡潔な説明を提供している引用を使用します。

それは変数の安全な公開についてです:

DCL の実際の問題は、同期せずに共有オブジェクト参照を読み取るときに発生する可能性がある最悪の事態は、古い値 (この場合は null ) を誤って参照することであるという想定です。その場合、DCL のイディオムは、ロックを保持した状態で再試行することにより、このリスクを補います。しかし、最悪の場合は実際にはかなり悪化します。参照の現在の値が表示されても、オブジェクトの状態の古い値が表示される可能性があります。つまり、オブジェクトが無効な状態または正しくない状態にあると見なされる可能性があります。

JMM (Java 5.0 以降) のその後の変更により、リソースが volatile にされた場合に DCL が機能するようになりました。通常、揮発性の読み取りは不揮発性の読み取りよりもわずかに高価であるため、これによるパフォーマンスへの影響はわずかです。

ただし、これは有用性がほとんどなくなったイディオムです。その動機となった力 (競合しない同期の遅さ、JVM の起動の遅さ) はもはや作用していないため、最適化としての効果が低下しています。遅延初期化ホルダー イディオムは同じ利点を提供し、理解しやすくなっています。

リスト16.6。遅延初期化ホルダー クラス イディオム。

public class ResourceFactory
    private static class ResourceHolder {
        public static Resource resource = new Resource();
    }

    public static Resource getResource() {
        return ResourceHolder.resource;
    }
}

それがやり方です。

于 2016-03-06T17:04:19.207 に答える
0

おそらく、java.util.concurrent.atomicのアトミックデータ型を使用する必要があります。

于 2009-10-27T23:27:38.910 に答える
0

二重チェックが使用される場合がいくつかあります。

  1. まず、シングルトンが本当に必要なく、多くのオブジェクトを作成および初期化しないためだけにダブルチェックが使用される場合。
  2. finalコンストラクター/初期化されたブロックの最後にフィールド セットがあります (これにより、以前に初期化されたすべてのフィールドが他のスレッドに表示されます)。
于 2013-04-23T19:32:44.630 に答える
0

私は二重チェックのロックイディオムについて調査してきましたが、私が理解したことから、あなたのコードは、テストクラスが不変でない限り、部分的に構築されたインスタンスを読み取る問題につながる可能性があります:

Java メモリ モデルは、不変オブジェクトを共有するための初期化の安全性を特別に保証します。

オブジェクト参照を公開するために同期を使用しない場合でも、安全にアクセスできます。

(非常にお勧めの本 Java Concurrency in Practice からの引用)

したがって、その場合、二重チェックのロックイディオムが機能します。

ただし、そうでない場合は、同期せずに変数インスタンスを返すことに注意してください。そのため、インスタンス変数が完全に構築されていない可能性があります (コンストラクターで提供された値ではなく、属性のデフォルト値が表示されます)。

ブール変数は、Test クラスが初期化される前に true に設定される可能性があるため、問題を回避するために何も追加しません (synchronized キーワードは並べ替えを完全に回避するわけではなく、一部の文によって順序が変わる可能性があります)。Java メモリ モデルには、それを保証する事前発生規則はありません。

32ビット変数はJavaでアトミックに作成されるため、ブール値を揮発性にしても何も追加されません。二重チェックのロックイディオムも同様に機能します。

Java 5 以降、インスタンス変数を volatile として宣言することで、この問題を修正できます。

この非常に興味深い記事で、二重チェックのイディオムについて詳しく読むことができます。

最後に、私が読んだいくつかの推奨事項:

  • シングルトン パターンを使用する必要があるかどうかを検討してください。これは、多くの人がアンチパターンと見なしています。可能であれば、依存性注入が推奨されます。これを確認してください。

  • 実装する前に、ダブル チェック ロックの最適化が本当に必要かどうかを慎重に検討してください。ほとんどの場合、努力する価値がないからです。また、静的フィールドで Test クラスを構築することを検討してください。遅延読み込みは、クラスの構築に多くのリソースが必要な場合にのみ役立ちますが、ほとんどの場合はそうではありません。

この最適化をまだ実行する必要がある場合は、このリンクを確認してください。これには、試みているものと同様の効果を達成するための代替手段がいくつかあります。

于 2015-08-22T20:52:04.440 に答える
0

"initialized" が true の場合、"instance" は完全に初期化する必要があります。1 プラス 1 は 2 と同じです:)。したがって、コードは正しいです。インスタンスは 1 回だけインスタンス化されますが、関数は 100 万回呼び出される可能性があるため、同期を 100 万マイナス 1 回チェックしなくてもパフォーマンスが向上します。

于 2012-10-17T04:38:17.157 に答える