A seemingly simple but not simple singleton pattern

Keywords: Programming Java Database jvm Attribute

Introduction

Today, I saw an article on the Public Number, which asked me how to implement the singleton mode without using synchronized and Lock locks. To be honest, before that, only two implementation modes of the singleton mode were known, and none of the others had been seen at all. Today, it's an eye-opening day. It's still a long way to go. Let's take this opportunity to make a summary.

Common singleton patterns

Solution based on volatile

This model is actually what we commonly call the Lazy Man Model, and also known as Double Check Lock. In multithreaded environments, in order to ensure that class initialization is only initialized once, we need to use lock mutex to ensure atomicity. This pattern is also easy to understand. The following code is given:

/*
    Solution based on volatile
 */
public class DoubleCheckedLocking {
    //volatile guarantees instance atomicity
    private static volatile Instance instance;

    public static Instance getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckedLocking.class) {
                if (instance == null) {
                    instance = new Instance();
                }
            }
        }
        return instance;
    }
}

class Instance { }

Delayed initialization

Next is the corresponding hungry mode, also known as delayed initialization. This mode is also well understood. Thread-safe singletons are implemented by class class loading mechanism.

public class Singleton { 
    private static Singleton instance = new Singleton();
    private Singleton (){}
    public static Singleton getInstance() {
      return instance;
    }
}

There is another variant of this scheme:

public class Singleton {
    private Singleton instance = null;
    static {
        instance = new Singleton();
    }
    private Singleton (){}
    public static Singleton getInstance() {
        return this.instance;
    }
}

In addition, there is a more advanced way of writing, using static internal classes:

/*
    Solution Based on Class Initialization
 */
public class InstanceFactory {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }

    public static Instance getInstance() {
        return InstanceHolder.instance;
    }
}

This approach is optimized by using lazy-loading. Instance class is loaded, but instance is not initialized immediately. Because the InstanceHolder class is not actively used, only when the getInstance method is invoked will the loaded InstanceHolder class be displayed to instantiate the instance.

CAS singleton mode

Above is the most basic way to realize the singleton mode. Next is the singleton mode of optimistic lock using CAS mechanism which we are familiar with.

/*
    CAS Implementing the singleton pattern
 */
public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();

    private Singleton() {}

    public static Singleton getInstance() {
        for (; ; ) {
            Singleton singleton = INSTANCE.get();
            if (singleton != null) {
                return singleton;
            }

            singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

The advantage of CAS is that it does not need to use traditional lock mechanism to ensure thread safety. CAS is a busy waiting algorithm, which relies on the implementation of the underlying hardware. Compared with lock, it has no additional consumption of thread switching and blocking, and can support a larger degree of parallelism.
An important disadvantage of CAS is that if the busy waiting is always unsuccessful (always in a dead loop), it will cause a large execution overhead to the CPU.

In addition, if N threads simultaneously execute to singleton = new Singleton(); there will be a large number of object creation, which may lead to memory overflow. Therefore, this implementation is not recommended.

Implementing singleton patterns with enumerated classes

Here comes the highlight. This can be the implementation method of the singleton pattern recommended by the famous Effective Java. Because of its complete function, concise use, free serialization mechanism, and absolute protection against multiple instances in the face of complex serialization or reflection attacks, the enumeration type of elements is considered by the author as the best way to implement Singleton.

Look at source code:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {}

It can be seen that it implements the Comparable and Serializable interfaces. In fact, Enum is a common class that inherits from the java.lang.Enum class. There are a lot of grammatical sugars in JDK5, and enumeration is one of them. Enumeration is also a kind of grammatical sugar.
Syntactic Sugar, also known as sugar-coated grammar, is a term invented by Peter J. Landin, a British computer scientist. It refers to a grammar added to a computer language. This grammar has no effect on the function of the language, but it is more convenient for programmers to use. It's just done on the compiler, but it doesn't provide the corresponding instruction set to handle it.

Implementing the singleton pattern with enumerated classes is very simple:

public enum Singleton{
    INSTANCE;
    
    public void whateverMethod(){}
}

Let's use the simulated enumeration class to implement the database connection scenario:

public enum SingletonOfEnum {
    DATASOURCE;
    private MYSQLConnection connection = null;

    private SingletonOfEnum() {
        connection = new MYSQLConnection();
    }

    public MYSQLConnection getConnection() {
        return connection;
    }

}

class MYSQLConnection{}

Main method:

public class Main {
    public static void main(String[] args) {
        MYSQLConnection c1 = SingletonOfEnum.DATASOURCE.getConnection();
        MYSQLConnection c2 = SingletonOfEnum.DATASOURCE.getConnection();
        MYSQLConnection c3 = SingletonOfEnum.DATASOURCE.getConnection();

        System.out.println(c1 == c2);
        System.out.println(c1 == c3);
        System.out.println(c2 == c3);
    }
}

Output results:

This shows that all three returns are the same instance!

So how do enumeration classes achieve thread safety in multi-threaded situations?

Decompiled code:

public final class DesignPattern.SingletonPattern.EnumSingleton.SingletonOfEnum extends java.lang.Enum<DesignPattern.SingletonPattern.EnumSingleton.SingletonOfEnum> {
  public static final DesignPattern.SingletonPattern.EnumSingleton.SingletonOfEnum DATASOURCE;

According to the decompiled code, DATASOURCE is declared static. According to the class loading process described in Hungry Man mode, we can know that virtual opportunity guarantees that the <clinit>() method of a class is locked and synchronized correctly in multi-threaded environment. So enumeration implementations are thread-safe when instantiated.

So what about serialization?

If you look at the Enum source code, you will find a valueOf() method:

public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
}

The Java specification stipulates that each enumeration type and its extremely defined enumeration variables are unique in the JVM, so Java makes special provisions for serialization and deserialization of enumeration types.
In serialization, Java simply outputs the name attribute of the enumerated object to the result, while in deserialization, it finds the enumerated object by name through java.lang.Enum's valueOf().
That is to say, for example, when serializing, only the name DATASOURCE is output, and when deserializing, the enumeration type is found by this name, so the deserialized instance will be the same as the object instance that was serialized before.

It can be concluded that the enumeration class itself guarantees the serialization of singletons.

 

 

Posted by slj90 on Wed, 24 Apr 2019 10:42:35 -0700