简体   繁体   中英

Does mutation of an non-thread-safe collection in a constructor need to be synchronized?

If I decide to use a non-thread-safe collection and synchronize its access, do I need to synchronize any mutation in the constructor? For example in the following code, I understand the reference to the list will be visible to all threads post-construction because it is final. But I don't know if this constitutes safe publication because the add in the constructor is not synchronized and it is adding a reference in ArrayList's elementData array, which is non-final.

private final List<Object> list;

public ListInConstructor()
{
    list = new ArrayList<>();
    // synchronize here?
    list.add(new Object());
}

public void mutate()
{
    synchronized (list)
    {
        if (list.checkSomething())
        {
            list.mutateSomething();
        }
    }
}

Update: The Java Language Specification states that the freeze making the changes visible must be at the end of the constructor, which means your code is correctly synchronized, see the answers from John Vint and Voo .

However you can also do this, which definitely works:

public ListInConstructor()
{
    List<Object> tmp = new ArrayList<>();
    tmp.add(new Object());
    this.list = tmp;
}

Here we mutate the list object before assigning it to the final field, and so the assignment will guarantee that any changes made to the list will also be visible.

17.5. final Field Semantics

The usage model for final fields is a simple one: Set the final fields for an object in that object's constructor; and do not write a reference to the object being constructed in a place where another thread can see it before the object's constructor is finished. If this is followed, then when the object is seen by another thread, that thread will always see the correctly constructed version of that object's final fields. It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are.

The highlighted sentence gives you a guarantee that this solution will work. Although, as pointed out at the start of the answer, the original has to work too, but I'll leave this answer here as the specification is slightly confusing. And because this "trick" also works when setting non-final but volatile fields (from any context, not just constructors).

According to the JLS

The usage model for final fields is a simple one: Set the final fields for an object in that object's constructor; and do not write a reference to the object being constructed in a place where another thread can see it before the object's constructor is finished.

Since the write to the List occurs prior to the constructor completing you are safe in mutating the list without additional synchronization.

edit: Based on Voo's comment I will make edit with the inclusion of final field freezing.

So reading more into 17.5.1 there is this entry

Given a write w, a freeze f, an action a (that is not a read of a final field), a read r1 of the final field frozen by f, and a read r2 such that hb(w, f), hb(f, a), mc(a, r1), and dereferences(r1, r2),

I interpret this as the action to modify the array happens-before the later derefencing of r2 which is the non-synchronized read after the freeze completes (the constructor exists).

Because the object is not inherently immutable, you must safely publish the object. As long as you do this, there is no need to do the mutations in the constructor in a synchronized block though.

Objects can be "safely published" in a number of ways. An example is passing them to another thread via a properly synchronized queue. For more details see Java Concurrency in Practice section 3.5.3 "Safe publication idioms" and 3.5.4 "Effectively Immutable Objects"

Ok so this is what JLS §17.5.1 has to say on the topic.

First of all:

Let o be an object, and c be a constructor for o in which a final field f is written. A freeze action on final field f of o takes place when c exits, either normally or abruptly .

So we know that in our code:

public ListInConstructor() {
    list = new ArrayList<>();
    list.add(new Object());
} // the freeze action happens here!

So now the interesting part:

Given a write w, a freeze f, an action a (that is not a read of a final field), a read r1 of the final field frozen by f, and a read r2 such that hb(w, f), hb(f, a), mc(a, r1), and dereferences(r1, r2), then when determining which values can be seen by r2, we consider hb(w, r2).

So let's do this one piece at a time:

We have hb(w,f), which means we write to the final field before leaving the constructor.

r1 is the read of the final field and dereferences(r1, r2). This means that r1 reads the final field and r2 then reads some value of this final field.

We further have an action (a read or write, but not a read of the final field) which has hb(f,a) and mc(a, r1). This means that the action happens after the constructor but can be seen by the read r1 afterwards.

And consequently it states that "we consider hb(w, r2)", which means that the write has to happen before the read to a value of the final field that was read with r1.

So the way I see it, it is clear that the object added to the list will have to be visible by any thread that can read list .

On a sidenote: HotSpot implements final field semantics by putting a memory barrier at the end of any constructor that contains a final field thereby guaranteeing this property in any case. Whether that's just an optimisation (Better to do to only a single barrier and that as far away from the write as possible) is another question.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM