Does object construction guarantee in practice that all threads see non-final fields initialized?

自闭症网瘾萝莉.ら 提交于 2020-12-29 04:00:55

问题


The Java memory model guarantees a happens-before relationship between an object's construction and finalizer:

There is a happens-before edge from the end of a constructor of an object to the start of a finalizer (§12.6) for that object.

As well as the constructor and the initialization of final fields:

An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.

There's also a guarantee about volatile fields since, there's a happens-before relations with regard to all access to such fields:

A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.

But what about regular, good old non-volatile fields? I've seen a lot of multi-threaded code that doesn't bother creating any sort of memory barrier after object construction with non-volatile fields. But I've never seen or heard of any issues because of it and I wasn't able to recreate such partial construction myself.

Do modern JVMs just put memory barriers after construction? Avoid reordering around construction? Or was I just lucky? If it's the latter, is it possible to write code that reproduces partial construction at will?

Edit:

To clarify, I'm talking about the following situation. Say we have a class:

public class Foo{
    public int bar = 0;

    public Foo(){
        this.bar = 5;
    }
    ...
}

And some Thread T1 instantiates a new Foo instance:

Foo myFoo = new Foo();

Then passes the instance to some other thread, which we'll call T2:

Thread t = new Thread(() -> {
     if (myFoo.bar == 5){
         ....
     }
});
t.start();

T1 performed two writes that are interesting to us:

  1. T1 wrote the value 5 to bar of the newly instantiated myFoo
  2. T1 wrote the reference to the newly created object to the myFoo variable

For T1, we get a guarantee that write #1 happened-before write #2:

Each action in a thread happens-before every action in that thread that comes later in the program's order.

But as far as T2 is concerned the Java memory model offers no such guarantee. Nothing prevents it from seeing the writes in the opposite order. So it could see a fully built Foo object, but with the bar field equal to equal to 0.

Edit2:

I took a second look at the example above a few months after writing it. And that code is actually guaranteed to work correctly since T2 was started after T1's writes. That makes it an incorrect example for the question I wanted to ask. The fix it to assume that T2 is already running when T1 is performing the write. Say T2 is reading myFoo in a loop, like so:

Foo myFoo = null;
Thread t2 = new Thread(() -> {
     for (;;) {
         if (myFoo != null && myFoo.bar == 5){
             ...
         }
         ...
     }
});
t2.start();
myFoo = new Foo(); //The creation of Foo happens after t2 is already running

回答1:


Taking your example as the question itself - the answer would be yes, that is entirely possible. The initialized fields are visible only to the constructing thread, like you quoted. This is called safe publication (but I bet you already knew about this).

The fact that you are not seeing that via experimentation is that AFAIK on x86 (being a strong memory model), stores are not re-ordered anyway, so unless JIT would re-ordered those stores that T1 did - you can't see that. But that is playing with fire, literately, this question and the follow-up (it's close to the same) here of a guy that (not sure if true) lost 12 milion of equipment

The JLS guarantees only a few ways to achieve the visibility. And it's not the other way around btw, the JLS will not say when this would break, it will say when it will work.

1) final field semantics

Notice how the example shows that each field has to be final - even if under the current implementation a single one would suffice, and there are two memory barriers inserted (when final(s) are used) after the constructor: LoadStore and StoreStore.

2) volatile fields (and implicitly AtomicXXX); I think this one does not need any explanations and it seems you quoted this.

3) Static initializers well, kind of should be obvious IMO

4) Some locking involved - this should be obvious too, happens-before rule...




回答2:


But anecdotal evidence suggests that it doesn't happen in practice

To see this issue, you have to avoid using any memory barriers. e.g. if you use thread safe collection of any kind or some System.out.println can prevent the problem occurring.

I have seen a problem with this before though a simple test I just wrote for Java 8 update 161 on x64 didn't show this problem.




回答3:


It seems there is no synchronization during object construction.

The JLS doesn't permit it, nor was I able to produce any signs of it in code. However, it's possible to produce an opposition.

Running the following code:

public class Main {
    public static void main(String[] args) throws Exception {
        new Thread(() -> {
            while(true) {
                new Demo(1, 2);
            }
        }).start(); 
    }
}

class Demo {
    int d1, d2;

    Demo(int d1, int d2) {
        this.d1 = d1;   

        new Thread(() -> System.out.println(Demo.this.d1+" "+Demo.this.d2)).start();

        try {
            Thread.sleep(500);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }

        this.d2 = d2;   
    }
}

The output would continuously show 1 0, proving that the created thread was able to access data of a partially created object.

However, if we synchronized this:

Demo(int d1, int d2) {
    synchronized(Demo.class) {
        this.d1 = d1;   

        new Thread(() -> {
            synchronized(Demo.class) {
                System.out.println(Demo.this.d1+" "+Demo.this.d2);
            }
        }).start();

        try {
            Thread.sleep(500);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }

        this.d2 = d2;   
    }
}

The output is 1 2, showing that the newly created thread will in fact wait for a lock, opposed to the unsynchronized exampled.

Related: Why can't constructors be synchronized?



来源:https://stackoverflow.com/questions/51695962/does-object-construction-guarantee-in-practice-that-all-threads-see-non-final-fi

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!