Computing Thoughts

Bruce Eckel's Programming Blog

Jan 13, 2017 - 5 minute read

Constructors Are Not Thread-Safe

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 CompletableFutures that run MakeObjects suppliers, then takes the results of each and uses the Guava library Sets.intersection() to find out how many ids 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.