2

たとえば、次のコードがあります。

Test t = new Test();

実際には次の 3 つの手順でバイト コードにコンパイルします。

1. mem = allocateMem() : allocate memory for Test and save it's address.
2. construct(mem) : construct the class Test
3. t = mem : point t to the mem

ここで、construct(mem) が非常に遅い場合、JIT は mem が完全に構築されるまでステップ 2 で待機するのでしょうか?

待たない場合(非同期)

それでは、使用前にメモリが完全に構築されていることをどのように保証できますか(シングルスレッド)?

待機する場合(同期)

では、なぜダブル チェック ロック (以下のコードとこの記事を参照) が失敗するのでしょうか?

class DB {
  private DB(){}
  private static DB instance;

  public static DB getInstance() {
    // First check
    if(instance == null ){
      synchronized(DB.class){
        // Second check
        if(instance == null) {
          instance = new Instance();
        }
      }
    }
    return instance;
  }
}

私が参照した記事では、上記のコードが完全に構築されていないインスタンスを返すことを指摘しています。

4

2 に答える 2

4

DCL が失敗する理由とその修正方法については、私がずっと前に StackOverflow で提供したこの回答を確認してください。


問題は同期/非同期ではありません。問題は、並べ替えと呼ばれるものです。

JVM 仕様では、事前発生関係と呼ばれるものを定義しています。単一のスレッド内で、ステートメント S1 がステートメント S2 の前にある場合、S1 は S2 の前に発生します。つまり、 S1 がメモリに対して行った変更はすべて S2 に表示されます。ステートメント S1 を S2 の前に実行する必要があるとは言っていないことに注意してください。S1がS2の前に実行されたかのように見えるべきだと言っているだけです。たとえば、次のコードを検討してください。

int x = 0;
int y = 0;
int z = 0;
x++;
y++;
z++;
z += x + y;
System.out.println(z);

ここでは、JVM が 3 つのインクリメント ステートメントを実行する順序は重要ではありません。唯一の保証は、実行時に x、y、および z の値がすべて 1 でなければならないということです。実際、並べ替えが先行発生関係にz += x + y違反しない場合、JVM は実際にステートメントを並べ替えることができます。これは、コードを少し並べ替えるだけでコードが最適化され、パフォーマンスが向上する場合があるためです。

欠点は、JVM が複数のスレッドを使用するときに非常に奇妙な結果につながる可能性のある方法で物事を並べ替えることができることです。例えば:

class Broken {
  private int value;
  private boolean initialized = false;
  public void init() {
    value = 5;
    initialized = true;
  }
  public boolean isInitialized() { return initialized; }
  public int getValue() { return value; }
}

スレッドが次のコードを実行しているとします。

while (!broken.isInitialized()) {
  Thread.sleep(1); // patiently wait...
}
System.out.println(broken.getValue());

ここで、別のスレッドが同じBrokenインスタンスで、

broken.init();

JVM は、最初に を実行し、次に を 5 に設定することinit()により、メソッド内のコードを並べ替えることができます。これが発生すると、初期化を待機している最初のスレッドが 0! を出力する可能性があります。修正するには、両方のメソッドに追加するか、フィールドに追加します。initialized = truevaluesynchronizedvolatileinitialized

DCL に戻ると、シングルトンの初期化が別の順序で実行される可能性があります。例えば:

1. mem = allocateMem() : allocate memory for Test and save it's address.
2. construct(mem) : construct the class Test
3. t = mem : point t to the mem

次のようになります。

1. mem = allocateMem() : allocate memory for Test and save it's address.
2. t = mem : point t to the mem
3. construct(mem) : construct the class Test

単一のスレッドの場合、両方のブロックが完全に同等であるためです。とはいえ、この種のシングルトン初期化は、シングル スレッド アプリケーションに対して完全に安全であることは間違いありません。ただし、複数のスレッドの場合、1 つのスレッドが部分的に初期化されたオブジェクトの参照を取得する可能性があります。

複数のスレッドを使用するときにステートメント間の事前発生関係を確保するには、ロックの取得/解放と揮発性フィールドの読み取り/書き込みの 2 つの可能性があります。DCL を修正するには、 singleton を保持するフィールドを宣言する必要がありますvolatile。これにより、シングルトンを保持するフィールドの読み取りの前に、シングルトンの初期化 (つまり、そのコンストラクターの実行) が確実に行われます。volatile が DCL をどのように修正するかについてのやや詳細な説明については、この回答の上部にリンクされている回答を確認してください。

于 2013-06-22T06:28:09.207 に答える
2

ここで、 が非常に遅い場合、完全に構築さconstruct(mem)れるまで JIT はステップ 2 で待機するのでしょうか?mem

JITによって生成されたコードについて話していると仮定すると...答えは、コードがその時点で必ずしも待機しているとは限らないということです。ステップ 3 の に何が来るかによって異なります。

それでは、使用前に完全に構​​築されていることをどのように保証できますmemか(シングルスレッド)?

要件は、そのスレッド1で観測された変数の値が、言語の指定されたセマンティクスと一致することです。つまり、「プログラムの順序」です。違いがなければ、JIT は命令を並べ替えることができます。具体的には、一部のフィールドのメモリへの書き込みが遅れていても問題ありません...スレッドがそれらの変数の値をメモリから読み取る必要がない場合。(コードはそれらをまったく読み取る必要がない場合もあれば、レジスタから読み取る場合もあれば、レベル 1 またはレベル 2 キャッシュからフェッチする場合もあります ....)

したがって、「どのようにそれを保証するのか」に対する簡単な答えは、実際の言語要件を満たす順序で命令を発行することによってそれを行うということです...あなたが仮定したより制限的なセマンティックではありません。


あなたの質問の 2 番目の部分 (DCL の実装に関するもの) は意味のないものとして扱っています。


1 - これはそのスレッドにのみ適用されます。JLS は、書き込みイベントと後続の読み取りイベントの間に「前に発生する」関係がない限り、他のスレッドに関して一貫性を保つためのそのような要件はないと述べています。

于 2013-06-22T06:39:20.257 に答える