When you imagine the construction process, it can be easy to think that it’s thread-safe. After all, no one can even see the new object before it finishes initialization, so how could there be contention over that object? Indeed, the Java Language Specification (JLS) confidently states:
“There is no practical need for a constructor to be synchronized, because it would lock the object under construction, which is normally not made available to other threads until all constructors for the object have completed their work.”
Unfortunately, object construction is as vulnerable to shared-memory concurrency problems as anything else. The mechanisms can be more subtle, however.
Consider the automatic creation of a unique identifier for each object using a
static
field. To test different implementations, we’ll start with an
interface:
// HasID.java
public interface HasID {
int getID();
}
Then implement that interface in an obvious way:
// StaticIDField.java
public class StaticIDField implements HasID {
private static int counter = 0;
private int id = counter++;
public int getID() { return id; }
}
This is about as simple and innocuous a class as you can imagine. It doesn’t even have an explicit constructor to cause problems. To see what happens when we make multiple concurrent tasks that create these objects, here’s a test harness:
// IDChecker.java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import java.util.concurrent.*;
import com.google.common.collect.Sets;
public class IDChecker {
public static int SIZE = 100_000;
static class MakeObjects
implements Supplier<List<Integer>> {
private Supplier<HasID> gen;
public MakeObjects(Supplier<HasID> gen) {
this.gen = gen;
}
@Override
public List<Integer> get() {
return
Stream.generate(gen)
.limit(SIZE)
.map(HasID::getID)
.collect(Collectors.toList());
}
}
public static void test(Supplier<HasID> gen) {
CompletableFuture<List<Integer>>
groupA = CompletableFuture
.supplyAsync(new MakeObjects(gen)),
groupB = CompletableFuture
.supplyAsync(new MakeObjects(gen));
groupA.thenAcceptBoth(groupB, (a, b) -> {
System.out.println(
Sets.intersection(
Sets.newHashSet(a),
Sets.newHashSet(b)).size());
}).join();
}
}
The MakeObjects
class is a Supplier
with a get()
that produces a
List<Integer>
. This List
is generated by extracting the id
from each
HasID
object. The test()
method creates two parallel CompletableFuture
s
that run MakeObjects
suppliers, then takes the results of each and uses the
Guava library Sets.intersection()
to find out how many id
s are common
between the two List<Integer>
(Guava is much faster than using retainAll()
).
Now we can test the StaticIDField
:
// TestStaticIDField.java
public class TestStaticIDField {
public static void main(String[] args) {
IDChecker.test(StaticIDField::new);
}
}
/* Output:
47643
*/
That’s a rather large number of duplicates. Clearly, a plain static int
is
not safe to use for construction. Let’s make it thread-safe using an
AtomicInteger
:
// GuardedIDField.java
import java.util.concurrent.atomic.*;
public class GuardedIDField implements HasID {
private static AtomicInteger counter =
new AtomicInteger();
private int id = counter.getAndAdd(1);
public int getID() { return id; }
public static void main(String[] args) {
IDChecker.test(GuardedIDField::new);
}
}
/* Output:
0
*/
Constructors have an even more subtle way to share state: through constructor arguments:
// SharedConstructorArgument.java
import java.util.concurrent.atomic.*;
interface SharedArg {
int get();
}
class Unsafe implements SharedArg {
private int i = 0;
public int get() { return i++; }
}
class Safe implements SharedArg {
private static AtomicInteger counter =
new AtomicInteger();
public int get() {
return counter.getAndAdd(1);
}
}
class SharedUser implements HasID {
private final int id;
public SharedUser(SharedArg sa) {
id = sa.get();
}
@Override
public int getID() { return id; }
}
public class SharedConstructorArgument {
public static void main(String[] args) {
Unsafe unsafe = new Unsafe();
IDChecker.test(() -> new SharedUser(unsafe));
Safe safe = new Safe();
IDChecker.test(() -> new SharedUser(safe));
}
}
/* Output:
47747
0
*/
Here, the SharedUser
constructors share the same argument. Even though
SharedUser
is using its argument in a completely innocent and reasonable
fashion, the way the constructor is called causes collisions. SharedUser
cannot even know it is being used this way, much less control it!
synchronized
constructors are not supported by the language, but it’s
possible to create your own using a synchronized
block. Although the
JLS states that “… it would lock the object under construction”, this is
not true—the constructor is effectively a static
method, so a
synchronized
constructor would actually lock through the class object. We
can reproduce this by creating our own static
object and locking on that:
// SynchronizedConstructor.java
import java.util.concurrent.atomic.*;
class SyncConstructor implements HasID {
private final int id;
private static Object constructorLock = new Object();
public SyncConstructor(SharedArg sa) {
synchronized(constructorLock) {
id = sa.get();
}
}
@Override
public int getID() { return id; }
}
public class SynchronizedConstructor {
public static void main(String[] args) {
Unsafe unsafe = new Unsafe();
IDChecker.test(() -> new SyncConstructor(unsafe));
}
}
/* Output:
0
*/
The shared use of the Unsafe
class is now safe.
An alternate approach is to make the constructors private
(thus preventing
inheritance) and provide a static
Factory Method to produce new objects:
// SynchronizedFactory.java
import java.util.concurrent.atomic.*;
class SyncFactory implements HasID {
private final int id;
private SyncFactory(SharedArg sa) {
id = sa.get();
}
@Override
public int getID() { return id; }
public static synchronized
SyncFactory factory(SharedArg sa) {
return new SyncFactory(sa);
}
}
public class SynchronizedFactory {
public static void main(String[] args) {
Unsafe unsafe = new Unsafe();
IDChecker.test(() ->
SyncFactory.factory(unsafe));
}
}
/* Output:
0
*/
By synchronizing the static
Factory Method you lock on the class object
during construction.
These examples emphasize how insidiously difficult it is to detect and manage shared state in concurrent Java programs. Even if you take the “share nothing” strategy, it’s remarkably easy for accidental sharing to take place.