December 21, 2016

Efficient Thread-safe Singleton in Java

Single-threaded Version

class Foo {
    private static Helper helper;
    public static Helper getHelper() {
        if (helper == null) {
            helper = new Helper();
        }
        return helper;
    }
}

The code works fine in single thread case. If multiple threads access getHelper() method simultaneously, they may try to create multiple objects or some of them may get an incompletely initialized object.

Naive Thread-safe Version

class Foo {
    private static Helper helper;
    public synchronized static Helper getHelper() {
        if (helper == null) {
            helper = new Helper();
        }
        return helper;
    }
}

The code is thread-safe, but synchronize the whole getHelper() method is unnecessary. Actually, only when multiple threads try to initialize helper, synchronization is needed. After helper initialized, simultaneous access to getHelper() works correctly without synchronization. It should return the same object.

Double-checked Locking Version

class Foo {
    private static volatile Helper helper;
    public static Helper getHelper() {
        Helper result = helper;
        if (result == null) {
            synchronized(Foo.class) {
                result = helper;
                if (result == null) {
                    result = new Helper();
                    helper = result;
                }
            }
        }
        return result;
    }
}

We optimized getHelper() method in the following manner:

  1. Check if helper is initialized, if so, return it immediately.
  2. Obtain lock.
  3. Double-check if helper is initialized, other thread may have already initialized it.
  4. Initialize helper.

The volatile keyword is necessary here. Prior to JDK 1.5, the compiler may reorder instructions. As a result, there's no way to guarantee an object's fields are all initialized before it's visible to other thread. Other thread may hold a reference to an incompletely initialized object, which may cause crashes.
Since JDK 1.5, volatile provide Happened-before guarantee, it make sure of these two things:

  1. If Thread A writes to a volatile variable and Thread B subsequently reads the same volatile variable, then all variables visible to Thread A before writing the volatile variable, will also be visible to Thread B after it has read the volatile variable.
  2. The reading and writing instructions of volatile variables cannot be reordered by the JVM.

There's no chance a thread see an incompletely initialized object. Double-checked locking idiom only works on JDK 1.5 or later. And it requires good understanding of java memory model.
Local variable result here slightly optimized the performance. Accessing volatile variable requires the variable to be read or written to main memory rather than CPU cache. If helper is already initialized, we only access volatile variable once due to "return result" instead of "return helper".

Initialization-on-demand Version

class Foo {
    private Foo() {
    
    }
    private static class FooHolder {
        private static final Foo INSTANCE = new Foo();
    }
    public static Foo getInstance() {
        return FooHolder.INSTANCE;
    }
}

Java Language Specification specified that the static class FooHolder is not initialized until the JVM determines it must be executed. Until getInstance() gets called, INSTANCE will not be initialized. Since the class initialization phase is guaranteed by the JLS to be serial, no further synchronization is required.

Enum Version

enum Foo {
    INSTANCE;
}

Implementing singleton with enum is recommended by Effective Java. Creation of enum is thread-safe, it's guaranteed by JVM. Compared with other implementation, enum version has unique advantages:

  1. It prevents creating another instance via reflection, although you can work around this in previous version by throwing exception in private constructor.
  2. If you're dealing with a serializable singleton, as deserialization will always create a new instance, you have to discard it by implementing readResolve() method and handling singleton state carefully. But with enum singleton, serialization and deserialization is guaranteed by JVM.