私は読んだ この質問 ダブルチェックロックを行う方法について:
// 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ドキュメント に基づいて機能すると信じています:
最終フィールドの使用モデルは単純なものです。オブジェクトのコンストラクターでオブジェクトの最終フィールドを設定します。また、オブジェクトのコンストラクターが完了する前に別のスレッドが参照できる場所に、構築中のオブジェクトへの参照を記述しないでください。これが続く場合、オブジェクトが別のスレッドに表示されると、そのスレッドは常にそのオブジェクトの最終フィールドの正しく構築されたバージョンを表示します。また、少なくとも最終フィールドと同じくらい最新のそれらの最終フィールドによって参照されるオブジェクトまたは配列のバージョンも表示されます。
まず最初に、あなたがしようとしていることはせいぜい危険です。決勝戦でカンニングをしようとすると少し緊張します。 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
をメソッド本体全体。 final
のFinalWrapper
は、null以外のwrapper
が見つかった場合に保証され、完全に構築され、すべてのSingleton
フィールドが表示されます-これは、 wrapper
のきちんとした読み取り。
値自体ではなく、フィールドのFinalWrapper
を引き継ぐことに注意してください。 instance
がFinalWrapper
なしで公開された場合、すべての賭けは無効になります(素人の言葉で言えば、それは時期尚早の公開です)。これがPublisher.publish
は機能しません。最後のフィールドに値を入力して読み戻し、安全でない方法で公開するだけでは安全ではありません。これは、裸のinstance
を書き出すのと非常によく似ています。
また、null wrapper
を検出し、その値を使用する場合は、ロックの下で「フォールバック」を読み取るように注意する必要があります。 -)。 returnステートメントでwrapper
の2回目(3回目)の読み取りを行うと、正当性が損なわれ、正当なレースに向けてセットアップされます。
編集:ちなみに、公開しているオブジェクトが内部でfinal
- sで覆われている場合は、FinalWrapper
の仲介者を切り取って、instance
自体。
編集2: LCK10-J。ダブルチェックされたロックイディオムの正しい形式を使用する 、およびそこにあるコメントでのいくつかの議論も参照してください。
volatile
またはラッパークラスのないコードのバージョンは、JVMが実行されている基盤となるオペレーティングシステムのメモリモデルに依存します。
ラッパークラスを含むバージョンは、 Initialization on Demand Holder デザインパターンとして知られる既知の代替手段であり、最初のアクセス時に任意のクラスが最大1回ロードされるClassLoader
コントラクトに依存します。 、そしてスレッドセーフな方法で。
volatile
の必要性開発者がコード実行について考える方法のほとんどは、プログラムがメインメモリにロードされ、そこから直接実行されるというものです。ただし、実際には、メインメモリとプロセッサコアの間に多数のハードウェアキャッシュが存在します。この問題は、各スレッドが別々のプロセッサで実行され、それぞれがスコープ内の変数の独自の独立したコピーを持つために発生します。論理的にはfield
を1つの場所として考えるのが好きですが、現実はより複雑です。
単純な(おそらく冗長ですが)例を実行するために、2つのスレッドと単一レベルのハードウェアキャッシュがあり、各スレッドがそのキャッシュにfield
の独自のコピーを持っているシナリオを考えます。したがって、すでにfield
の3つのバージョンがあります。1つはメインメモリに、1つは最初のコピーに、もう1つは2番目のコピーにあります。これらをfield
と呼びますM、field
あ、およびfield
B それぞれ。
field
M = null
field
あ = null
field
B = null
field
を見つけますあ 無効です。this
のロックを取得します。field
を見つけますB 無効です。this
のロックを取得しようとしますが、スレッドAによって保持されていることを検出します。スレッドBはスリープします。field
を見つけますあ 無効です。field
を割り当てますあ 値fieldType1
およびロックを解放します。 field
はvolatile
ではないため、この割り当ては伝達されません。field
M = null
field
あ = fieldType1
field
B = null
this
のロックを取得します。field
を見つけますB 無効です。field
を割り当てますB 値fieldType2
で、ロックを解除します。field
M = null
field
あ = fieldType1
field
B = fieldType2
field
M = fieldType1
field
あ = fieldType1
field
B = fieldType2
field
M = fieldType2
field
あ = fieldType1
field
B = fieldType2
上記の質問に対するコメント投稿者の1人として、volatile
を使用すると、書き込みが確実に表示されます。これを保証するために使用されるメカニズムがわかりません。変更が各コピーに伝播される可能性があります。そもそもコピーが作成されることはなく、field
へのすべてのアクセスが反対である可能性があります。メインメモリ。
これに関する最後の注意:結果はシステムに依存することを前述しました。これは、基盤となるさまざまなシステムがメモリモデルに対して楽観的でないアプローチを取り、スレッド間で共有されるallメモリをvolatile
として扱うか、ヒューリスティックを適用して特定の参照はvolatile
として扱われるかどうかに関係なく、メインメモリへの同期のパフォーマンスが犠牲になります。これにより、これらの問題のテストが悪夢になる可能性があります。競合状態をトリガーするのに十分な大きさのサンプルに対して実行する必要があるだけでなく、条件をトリガーしないように十分に保守的なシステムでテストしている場合があります。
ここで指摘したい主なことは、本質的にシングルトンをミックスにこっそり入れているため、これが機能することです。 ClassLoader
コントラクトは、Class
のインスタンスが多数存在する可能性がある一方で、どのタイプのA
でも使用できるClass<A>
のインスタンスは1つだけであることを意味します。最初の参照/遅延初期化時に最初にロードされます。実際、クラスの定義の静的フィールドは、そのクラスに関連付けられたシングルトンのフィールドであり、そのシングルトンとクラスのインスタンスの間でメンバーアクセス権限が増加していると考えることができます。
引用 「ダブルチェックロックが壊れている」宣言 @Kicsiが言及しました。最後のセクションは次のとおりです。
不変オブジェクトのロックの二重チェック
Helperが不変オブジェクトで、Helperのすべてのフィールドがfinalである場合、ダブルチェックロックは、揮発性フィールドを使用しなくても機能します。不変オブジェクト(文字列や整数など)への参照は、intまたはfloatとほぼ同じように動作する必要があるという考え方です。不変オブジェクトへの参照の読み取りと書き込みはアトミックです。
(強調は私のものです)
FieldHolder
は不変であるため、実際にはvolatile
キーワードは必要ありません。他のスレッドは常に適切に初期化されたFieldHolder
を参照します。したがって、私が理解している限り、FieldType
は、他のスレッドからFieldHolder
を介してアクセスできるようになる前に、常に初期化されます。
ただし、FieldType
が不変でない場合は、適切な同期が必要です。結果として、volatile
キーワードを回避することで多くのメリットが得られるかどうかはわかりません。
ただし、不変であれば、上記の引用に従って、FieldHolder
はまったく必要ありません。
遅延初期化に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;
}
}