23

Someone somewhere told me that Java constructors are synchronized so that it can't be accessed concurrently during construction, and I was wondering: if I have a constructor that stores the object in a map, and another thread retrieves it from that map before its construction is finished, will that thread block until the constructor completes?

Let me demonstrate with some code:

public class Test {
    private static final Map<Integer, Test> testsById =
            Collections.synchronizedMap(new HashMap<>());
    private static final AtomicInteger atomicIdGenerator = new AtomicInteger();
    private final int id;

    public Test() {
        this.id = atomicIdGenerator.getAndIncrement();
        testsById.put(this.id, this);
        // Some lengthy operation to fully initialize this object
    }

    public static Test getTestById(int id) {
        return testsById.get(id);
    }
}

Assume that put/get are the only operations on the map, so I won't get CME's via something like iteration, and try to ignore other obvious flaws here.

What I want to know is if another thread (that's not the one constructing the object, obviously) tries to access the object using getTestById and calling something on it, will it block? In other words:

Test test = getTestById(someId);
test.doSomething(); // Does this line block until the constructor is done?

I'm just trying to clarify how far the constructor synchronization goes in Java and if code like this would be problematic. I've seen code like this recently that did this instead of using a static factory method, and I was wondering just how dangerous (or safe) this is in a multi-threaded system.

4

6 に答える 6

25

Javaコンストラクターは同期されているため、構築中に同時にアクセスできないとどこかで誰かが言った

これは確かにそうではありません。コンストラクターとの暗黙の同期はありません。複数のコンストラクターが同時に発生する可能性があるだけでなく、たとえば、構築中のコンストラクターへの参照を使用しthisてコンストラクター内でスレッドをフォークすることにより、同時実行の問題が発生する可能性があります。

オブジェクトをマップに格納するコンストラクターがあり、その構築が完了する前に別のスレッドがそのマップからオブジェクトを取得した場合、そのスレッドはコンストラクターが完了するまでブロックされますか?

いいえ、そうではありません。

スレッド化されたアプリケーションでのコンストラクターの大きな問題は、Java メモリ モデルの下で、オブジェクト参照が作成されてコンストラクターが終了した 後に実行されるように、コンパイラーがコンストラクター内の操作を並べ替える権限を持っていることです。finalフィールドは、コンストラクターが終了するまでに完全に初期化されることが保証されますが、他の「通常の」フィールドは保証されません。

あなたの場合、あなたはあなたTestを同期マップに入れてから初期化続けているので、@Timが述べたように、これにより他のスレッドがおそらく半初期化された状態でオブジェクトを取得できるようになります。1 つの解決策は、staticメソッドを使用してオブジェクトを作成することです。

private Test() {
    this.id = atomicIdGenerator.getAndIncrement();
    // Some lengthy operation to fully initialize this object
}

public static Test createTest() {
    Test test = new Test();
    // this put to a synchronized map forces a happens-before of Test constructor
    testsById.put(test.id, test);
    return test;
}

私のサンプルコードは、コンストラクターが完了し、メモリが同期されているsynchronizedことを保証する呼び出しを行う同期マップを扱っているため、機能します。Test

あなたの例の大きな問題は、「前に発生する」保証(コンストラクターがTestマップに入れられる前に終了しない可能性がある)とメモリ同期(構築スレッドと取得スレッドがTestインスタンスの異なるメモリを参照する可能性がある)の両方です。コンストラクターの外側を移動するputと、両方が同期マップによって処理されます。synchronized コンストラクターがマップに配置され、メモリが同期される前にコンストラクターが終了したことを保証するために、どのオブジェクト上にあるかは問題ではありません。

コンストラクターの最後で呼び出しtestsById.put(this.id, this);た場合、実際には問題ないかもしれませんが、これは良い形式ではなく、少なくとも注意深いコメント/ドキュメントが必要です。クラスがサブクラス化され、初期化がsuper(). 私が示したstatic解決策は、より良いパターンです。

于 2012-09-26T22:25:52.073 に答える
15

Javaコンストラクターは同期されているとどこかで誰かが言った

「どこかの誰か」は深刻な誤解です。コンストラクターは同期されません。証拠:

public class A
{
    public A() throws InterruptedException
    {
        wait();
    }

    public static void main(String[] args) throws Exception
    {
        A a = new A();
    }
}

このコードは呼び出し時にスローjava.lang.IllegalMonitorStateExceptionされwait()ます。有効な同期があったとしても、そうではありません。

意味がありません。それらを同期する必要はありません。コンストラクターは の後にのみ呼び出すことができnew(),、定義により、 の各呼び出しはnew()異なる値を返します。したがって、コンストラクターが 2 つのスレッドによって同時に呼び出される可能性はゼロですthis。したがって、コンストラクターの同期は必要ありません。

オブジェクトをマップに格納するコンストラクターがあり、その構築が完了する前に別のスレッドがそのマップからオブジェクトを取得した場合、そのスレッドはコンストラクターが完了するまでブロックされますか?

いいえ、なぜそれをするのでしょうか? 誰がそれをブロックしますか?このようにコンストラクタから 'this' を逃がすのはお粗末です: 他のスレッドがまだ構築中のオブジェクトにアクセスできるようになります。

于 2012-09-27T10:28:35.027 に答える
13

あなたは誤解されています。あなたが説明することは、実際には不適切な公開と呼ばれ、Java Concurrency In Practice の本で詳しく説明されています。

そうです、別のスレッドがオブジェクトへの参照を取得し、初期化が完了する前にそれを使用しようとする可能性があります。しかし、待ってください、この答えを考えるとさらに悪化します: https ://stackoverflow.com/a/2624784/122207 ...基本的に、参照割り当てとコンストラクターの完了の並べ替えが発生する可能性があります。参照されている例では、1 つのスレッドが新しいインスタンスを割り当て、別のスレッドが適切なタイミングでh = new Holder(i)呼び出して、のコンストラクターで割り当てられたメンバーの 2 つの異なる値を取得できます。h.assertSanity()nHolder

于 2012-09-26T22:26:05.803 に答える
2

コンストラクターは他のメソッドと同様であり、追加の同期はありません (finalフィールドの処理を除く)。

this後で公開された場合、コードは機能します

public Test() 
{
    // Some lengthy operation to fully initialize this object

    this.id = atomicIdGenerator.getAndIncrement();
    testsById.put(this.id, this);
}
于 2012-09-26T22:52:38.497 に答える
1

この質問には回答がありますが、問題に貼り付けられたコードは、この参照がコンストラクターからエスケープされるため、安全な構築手法に従っていません。記事でブライアン・ゲッツが提示した美しい説明を共有したいと思います:「Java の理論と実践: IBM developerWorks Web サイトの「安全な構築手法」を参照してください

于 2013-12-07T12:24:49.433 に答える
-1

安全ではありません。JVMには追加の同期はありません。あなたはこのようなことをすることができます:

public class Test {
    private final Object lock = new Object();
    public Test() {
        synchronized (lock) {
            // your improper object reference publication
            // long initialization
        }
    }

    public void doSomething() {
        synchronized (lock) {
            // do something
        }
    }
}
于 2012-09-26T22:32:52.170 に答える