Every once in a while, the thread-safely of Java object constructors comes up. More specifically, it’s not so much about the process of object construction but rather the visibility of writes triggered by that process in relation to other threads.
What if a JVM implementation were to allocate memory for the new instance, store the new reference value and only then execute the constructor? In that case, another thread could observe a partially uninitialized object. What are the guarantees provided by the Java memory model and would that represent a violation?
It’s all about the actual reference assignment. Constructors themselves do not come with a guarantee that all writes happen before the write of the object reference. If the reference is not assigned to a volatile
or final
field, the JIT and the CPU are free to assign the reference before object construction. In the context of a single thread of execution, that’s an optimization decision the JIT can easily make as long as the program order is not violated. In other words, reordering performed by the compiler and CPU must produce the same results as executing the code in its original order.
A prominent example affected by constructor thread-safety is the double-checked locking pattern (lazy initialization not requiring a lock after the initialization phase), which, if implemented as follows, suffers from a concurrency issue and is not thread-safe. Another thread may see a partially constructed Singleton
instance because the Java memory model does not mandate any specific memory order for normal reads and writes.
private Singleton singleton;
public Singleton getInstance() {
if (singleton == null) {
synchronized (this) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
Before Java 1.5, this scenario was not affected by volatile
and final
, the memory model did not mandate any specific order in respect to other threads of execution.
With Java 1.5, the memory model was changed in respect to volatile
and final
fields to address this situation. With the new model, volatile writes have release semantics and volatile reads have acquire semantics. Provided volatile
is used for singleton
, this pattern works as expected because the memory model guarantees the expected order of events:
tmp = new Singleton();
// Implicit release memory barrier caused by volatile.
// Constructor reads and writes must not be moved
// after the volatile write by the JIT and CPU.
singleton = tmp;
Release semantics enforced by a release memory barrier prevent memory reordering of any read or write that precedes it in program order with any write that follows it in program order. This is equivalent to a combination of LoadStore
and StoreStore
memory barriers. Consequently, reads and writes belonging to Singleton
object construction must not be moved after the volatile singleton
write.
tmp = singleton;
// Implicit acquire memory barrier caused by volatile.
// Singleton reads and writes must not be moved
// before the volatile read by the JIT and CPU.
if (tmp == null) {
synchronized (this)
if (tmp == null) {
Acquire semantics enforced by an acquire memory barrier prevent memory reordering of any read that precedes it in program order with any read or write that follows it in program order. This is equivalent to a combination of LoadLoad
and LoadStore
memory barriers. Consequently, Singleton
reads and writes must not be moved before the volatile singleton
read.
It’s worth noting that in all versions of Java, volatile reads and writes are totally-ordered. All threads observe the same volatile read/write order. To achieve that, either a volatile write precedes a StoreLoad
memory barrier or a volatile read follows a StoreLoad
memory barrier. On x86, only the StoreLoad
memory barrier emits an instruction, other barriers have to be considered during JIT reordering.
Similarly, the semantics in terms of final
fields have changed with Java 1.5. JSR133, which introduced the memory model changes, used the following example to illustrate the problem:
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3;
y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if (f != null) {
int i = f.x;
int j = f.y;
}
}
}
Given two threads, thread A calling writer()
and thread B calling reader()
, the natural assumption would be that thread B is guaranteed to see the values 3
for i
and and 0
or 4
for j
. Due to reordering, thread B could see 0
instead - a clear violation of the premise of final
, not in terms of the original memory model but in respect to the higher-level contract of final
to represent immutable constant values.
To address this, Java 1.5 and later specify this guarantee:
[…] 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. […]
The implementation uses a StoreStore
memory barrier to prevent the write of x
from moving after the assignment of f
. Default values of y
can still be observed.
In Java 9, java.lang.invoke.VarHandle
was introduced to provide access to acquire/release and volatile semantics. VarHandle
is comparable to C++11’s std::atomic
in that it provides atomic primitives and memory order control including explicit memory barriers.
The Java object constructor is not inherently thread-safe. With the help of volatile
, final
, and VarHandle
, required guarantees can be established. For most common use cases, alternative patterns exist that do not require dealing with these kinds of low-level details. Whenever possible, prefer not to roll your own lock-free code to reduce code complexity and maximize the probability of correctness.