17

次のような Java オブジェクトを作成しているとします。

SomeClass someObject = null;
someObject = new SomeClass();

someObject が非 null になるのはどの時点ですか? SomeClass()コンストラクターが実行される前ですか、それとも後ですか?

少し明確にするために、コンストラクターが完了の途中でsomeObjectある間に別のスレッドが null であるかどうかを確認する場合、それは null か非 null でしょうか?SomeClass()

また、次のように作成された場合の違いは何でしょうsomeObject:

SomeClass someObject = new SomeClass();

someObjectnullになることはありますか?

4

6 に答える 6

16

someObject別のスレッドが「構築中に」変数をチェックした場合、(メモリモデルの癖により) 部分的に初期化されたオブジェクトが表示される可能性があると思います。新しい (Java 5 の) メモリ モデルは、オブジェクトが他のスレッドから見えるようになる前に、すべてのfinalフィールドをその値に設定する必要があることを意味します (ただし、新しく作成されたオブジェクトへの参照が他のスレッドのコンストラクタからエスケープされない限り)。方法) しかし、それ以上の保証はあまりありません。

基本的に、適切なロックなしでデータを共有しないでください (または静的初期化子などによって与えられる保証) :) 真剣に、一般的なロックフリー プログラミングと同様に、メモリ モデルは非常にトリッキーです。これが可能になるのを避けるようにしてください。

論理的には、代入はコンストラクターの実行に発生します。そのため、同じスレッドから変数を観察すると、コンストラクターの呼び出し中に null になります。ただし、私が言うように、メモリ モデルには奇妙な点があります。

編集: ダブルチェック ロックの目的で、フィールドがJava 5 以降を使用している場合volatileは、これを回避できます。Java 5 より前のメモリ モデルは、これに対して十分に強力ではありませんでした。ただし、パターンを正確に正しく取得する必要があります。詳細については、Effective Java、第 2 版、項目 71 を参照してください。

編集:アーロンのインライン化が単一のスレッドで表示されることに反対する私の理由は次のとおりです。次があるとします。

public class FooHolder
{
    public static Foo f = null;

    public static void main(String[] args)
    {
        f = new Foo();
        System.out.println(f.fWasNull);
    }
}

// Make this nested if you like, I don't believe it affects the reasoning
public class Foo
{
    public boolean fWasNull;

    public Foo()
    {
        fWasNull = FooHolder.f == null;
    }
}

これは常に報告されると思いますtrueセクション 15.26.1から:

それ以外の場合は、次の 3 つの手順が必要です。

  • 最初に、左側のオペランドが評価されて変数が生成されます。この評価が突然完了すると、代入式は同じ理由で突然完了します。右側のオペランドは評価されず、代入は行われません。
  • それ以外の場合は、右側のオペランドが評価されます。この評価が突然完了すると、代入式は同じ理由で突然完了し、代入は発生しません。
それ以外の場合、右側のオペランドの値は左側の変数の型に変換され、適切な標準値セット (拡張指数値セットではない) への値セット変換 (§5.1.13) を受けます。変換の結果は変数に格納されます。

次に、セクション 17.4.5から:

2 つのアクションは、先行発生関係によって順序付けできます。あるアクションが別のアクションの前に発生する場合、最初のアクションは 2 番目のアクションよりも前に表示され、順序付けされます。

x と y の 2 つのアクションがある場合、hb(x, y) と書き、x が y の前に発生することを示します。

  • x と y が同じスレッドのアクションであり、プログラムの順序で x が y の前にある場合、hb(x, y) になります。
  • オブジェクトのコンストラクターの最後から、そのオブジェクトのファイナライザー (§12.6) の開始までの先行発生エッジがあります。
  • アクション x が後続のアクション y と同期する場合、hb(x, y) も得られます。
  • hb(x, y) と hb(y, z) の場合、hb(x, z) です。

2 つのアクション間に事前発生関係が存在するからといって、必ずしも実装でその順序で実行する必要があるとは限らないことに注意してください。並べ替えが合法的な実行と一致する結果をもたらす場合、それは違法ではありません。

つまり、単一のスレッド内でも奇妙なことが起こっても問題ありませんが、それが観察可能であってはなりません。この場合、違い観察可能であるため、違法であると私は信じています。

于 2009-03-25T14:44:11.870 に答える
9

someObjectnull工事中のいずれかの時点で非になります。通常、次の 2 つのケースがあります。

  1. オプティマイザーはコンストラクターをインライン化しました
  2. コンストラクターはインライン化されていません。

最初のケースでは、VM は次のコード (疑似コード) を実行します。

someObject = malloc(SomeClass.size);
someObject.field = ...
....

したがって、この場合someObjectは ではなくnull 100% 初期化されていないメモリを指しています。つまり、すべてのコンストラクタ コードが実行されているわけではありません! これが、ダブルチェック ロックが機能しない理由です。

2 番目のケースでは、コンストラクターからのコードが実行され、(通常のメソッド呼び出しと同様に) 参照が返され、すべての init コードが実行されたに someObject が参照の値に設定されます。

someObject問題は、早期に割り当てないように Java に指示する方法がないことです。たとえば、次のことを試すことができます。

SomeClass tmp = new SomeClass();
someObject = tmp;

しかし、tmp は使用されていないため、オプティマイザーはそれを無視することができ、上記と同じコードが生成されます。

したがって、この動作は、オプティマイザーがより高速なコードを生成できるようにするためのものですが、マルチスレッド コードを作成するときに厄介な問題を引き起こす可能性があります。シングル スレッド コードでは、コンストラクターが終了するまでコードが実行されないため、これは通常問題になりません。

[編集] 何が起こっているかを説明する良い記事があります: http://www.ibm.com/developerworks/java/library/j-dcl.html

PS: Joshua Bloch による本「Effective Java, Second Edition 」には、Java 5 以降のソリューションが含まれています。

private volatile SomeClass field;
public SomeClass getField () {
    SomeClass result = field;
    if (result == null) { // First check, no locking
        synchronized(this) {
            result = field;
            if (result == null) { // second check with locking
                field = result = new SomeClass ();
            }
        }
    }
    return result;
}

奇妙に見えますが、すべての Java VM で動作するはずです。すべてのビットが重要であることに注意してください。二重割り当てを省略すると、パフォーマンスが低下するか、オブジェクトが部分的に初期化されます。完全な説明については、本を購入してください。

于 2009-03-25T14:46:02.817 に答える
2

someObject型のコンストラクターからポインター値が割り当てられるまで、null ポインターになります。割り当ては右から左に行われるため、コンストラクタがまだ実行されている間に別のスレッドがチェックする可能性があります。someObjectこれは、変数へのポインターの割り当ての前であるため、someObjectnull のままです。

于 2009-03-25T14:44:47.920 に答える
0

別のスレッドからは、コンストラクターの実行が完了するまで、オブジェクトは引き続き null に見えます。これが、構築が例外によって終了した場合、参照が null のままになる理由です。

Object o = null;
try {
    o = new CtorTest();
} catch (Exception e) {
    assert(o == null); // i will be null
}

どこ

class CtorTest {
    public CtorTest() {
        throw new RuntimeException("Ctor exception.");
    }
}

構築中のオブジェクトではなく、必ず別のオブジェクトで同期してください。

于 2009-03-25T15:01:15.743 に答える
-1

最初の例: someObject は、コンストラクターが完了した後に非 null になります。別のスレッドからチェックすると、コンストラクターの終了後に someObject が非 null になります。異なるスレッドから同期されていないオブジェクトにアクセスしないように注意してください。そのため、実際のコードでは例をそのように実装しないでください。

2 番目の例では、someObject は、SomeClass 自体が構築された後に構築され、someObject が作成され、新しく作成されたオブジェクトで初期化されるため、null になることはありません。スレッドについても同様です。同期せずに異なるスレッドからこの変数にアクセスしないでください。

于 2009-03-25T14:45:32.867 に答える
-1

コンストラクターの実行が完了するまでオブジェクトが nullであることを示すテスト コードを次に示します。

public class Test {

  private static SlowlyConstructed slowlyConstructed = null;

  public static void main(String[] args) {
    Thread constructor = new Thread() {
      public void run() {
        Test.slowlyConstructed = new SlowlyConstructed();
      }
    };
    Thread checker = new Thread() {
      public void run() {
        for(int i = 0; i < 10; i++) {
          System.out.println(Test.slowlyConstructed);
          try { Thread.sleep(1000); }
          catch(Exception e) {}
        }
      }
    };

    checker.start();
    constructor.start();
  }

  private static class SlowlyConstructed {
    public String s1 = "s1 is unset";
    public String s2 = "s2 is unset";

    public SlowlyConstructed() {
      System.out.println("Slow constructor has started");
      s1 = "s1 is set";
      try { Thread.sleep(5000); }
      catch (Exception e) {}
      s2 = "s2 is set";
      System.out.println("Slow constructor has finished");
    }

    public String toString() {
      return s1 + ", " + s2;
    }
  }
}

出力:

ヌル
遅いコンストラクターが開始されました
ヌル
ヌル
ヌル
ヌル
ヌル
遅いコンストラクターが終了しました
s1 が設定され、s2 が設定されます
s1 が設定され、s2 が設定されます
s1 が設定され、s2 が設定されます
s1 が設定され、s2 が設定されます
于 2009-03-25T15:01:43.077 に答える