Completely play with singleton mode

Posted by Ruski on Thu, 10 Feb 2022 21:29:08 +0100

  • Hungry Chinese style: class is initialized when loading. There is no problem of concurrent access and there will be a waste of resources
  • Lazy type: delay loading and instantiate the object only when it is used. There is a problem of concurrent access and high resource utilization
  • Double detection lock: the synchronized keyword is used to solve the problem of lazy concurrent access, and the volatile keyword is used to solve the problem of instruction rearrangement
  • Static inner class: merging the advantages of concurrent and efficient calls and delayed loading
  • Enumeration singleton: the implementation is simple. Enumeration itself is a singleton mode. The JVM provides guarantee fundamentally! Avoid vulnerabilities through reflection and deserialization! But you cannot delay loading.

1. Hungry Han style

package Singleton mode;

//Hungry Han style
public class Hungry {
    private static Hungry hungry = new Hungry();

    //Constructor private
    private Hungry() {
    }

    public static Hungry getInstance() {
        return hungry;
    }
}

Advantages: the static variable will be initialized when the class is loaded, which does not involve the problem of multiple threads accessing the object. The synchronized keyword can be omitted

Disadvantages: objects are created during class initialization. If you just load this class instead of calling getInstance(), or even never call it, it will cause a waste of resources!

2. Lazy style

package Singleton mode;

//Lazy style
public class Lazy {
    private static Lazy lazy;

    private Lazy() {

    }

    public static Lazy getInstance() {
        if (lazy == null)
            lazy = new Lazy();
        return lazy;
    }
}

Advantages: delay loading, instantiate the object only when it is really used, and improve the utilization of resources

Disadvantages: there is a problem of concurrent access. The following tests the concurrent access

package Singleton mode;

//Lazy style
public class Lazy {
    private static Lazy lazy;

    private Lazy() {
        System.out.println("Create sample");
    }

    public static Lazy getInstance() {
        if (lazy == null)
            lazy = new Lazy();
        return lazy;
    }

    public static void main(String[] args) {
        //Concurrent access of 10 threads
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Lazy.getInstance();
            }).start();
        }
    }
}


According to the results, it can be seen that five threads have printed the results, that is to say, five initializations have been carried out, which is a very large vulnerability and there is a problem of concurrent access

3. Double detection lock type

In order to solve the problem of lazy concurrent access, the synchronized keyword is added

package Singleton mode;

//Double detection lock
public class DoubleLock {
    private static DoubleLock doubleLock;

    private DoubleLock() {
        System.out.println("Create sample");
    }

    public static DoubleLock getInstance() {
        if (doubleLock == null) {
            synchronized (Lazy.class) {
                if (doubleLock == null)
                    doubleLock = new DoubleLock();
            }
        }
        return doubleLock;
    }

    public static void main(String[] args) {
        //Concurrent access of 10 threads
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                DoubleLock.getInstance();
            }).start();
        }
    }
}


According to the printing results, the problem of concurrent access is solved; However, there will still be problems, because the new object is not a complete atomic operation, but is divided into the following three parts:

  1. Allocate memory space
  2. Execute the construction method and initialize the object
  3. Point this object to this space

When A single thread A is executed, it can be executed in sequence 123 or 132 due to instruction rearrangement; However, if thread A executes in 132 order and A thread B comes at 3, the object has pointed to the allocated space. Therefore, if B judges that the object is not null, it will directly return the object, but the object has not been initialized, resulting in an error

Therefore, instruction rearrangement will also lead to errors. Therefore, the complete double detection lock also adds the Volatile keyword to avoid instruction rearrangement. The complete code is as follows:

package Singleton mode;

//Double detection lock
public class DoubleLock {
    private volatile static DoubleLock doubleLock;

    private DoubleLock() {
        System.out.println("Example creation");
    }

    public static DoubleLock getInstance() {
        if (doubleLock == null) {
            synchronized (Lazy.class) {
                if (doubleLock == null)
                    doubleLock = new DoubleLock();
            }
        }
        return doubleLock;
    }
}

4. Static internal class

package Singleton mode;

public class InnerClass {
    private InnerClass() {

    }

    //Create objects in static internal classes
    public static class inner {
        private static final InnerClass innerClass = new InnerClass();
    }

    public static InnerClass getInstance() {
        return inner.innerClass;
    }
}
  1. Delay loading. Only when getinstance() is really called will the static inner class be loaded.
  2. Thread safe. Instance is of static final type, which ensures that only such an instance exists in memory and can only be assigned once, thus ensuring thread safety.
  3. It has the advantages of concurrent and efficient invocation and delayed loading

- Reflection destruction singleton mode, introducing enumeration singleton

The double detection lock single case is destroyed by reflection

package Singleton mode;

import java.lang.reflect.Constructor;

//Double detection lock
public class DoubleLock {
    private volatile static DoubleLock doubleLock;

    private DoubleLock() {
        System.out.println("Create sample");
    }

    public static DoubleLock getInstance() {
        if (doubleLock == null) {
            synchronized (Lazy.class) {
                if (doubleLock == null)
                    doubleLock = new DoubleLock();
            }
        }
        return doubleLock;
    }

    public static void main(String[] args) throws Exception {
        DoubleLock instance1 = doubleLock.getInstance();
        Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        DoubleLock instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}


According to the results, we can see that two instances are created, that is, the single instance mode is destroyed. How to solve it?

Locks can be placed in private constructs

package Singleton mode;

import java.lang.reflect.Constructor;

//Double detection lock
public class DoubleLock {
    private volatile static DoubleLock doubleLock;

    private DoubleLock() {
        synchronized (DoubleLock.class){
            if(doubleLock!=null){
                throw new RuntimeException("Do not attempt to use reflection to break exceptions");
            }
        }
        System.out.println("Create sample");
    }

    public static DoubleLock getInstance() {
        if (doubleLock == null) {
            synchronized (Lazy.class) {
                if (doubleLock == null)
                    doubleLock = new DoubleLock();
            }
        }
        return doubleLock;
    }

    public static void main(String[] args) throws Exception {
        DoubleLock instance1 = doubleLock.getInstance();
        Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        DoubleLock instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}


According to the results, we can see that the destruction of singleton mode is avoided? However, one of the above two objects is obtained through single instance and the other is obtained through reflection;

What if both objects are acquired through reflection?

public static void main(String[] args) throws Exception {
    Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);
    constructor.setAccessible(true);
    DoubleLock instance1= constructor.newInstance();
    DoubleLock instance2 = constructor.newInstance();
    System.out.println(instance1);
    System.out.println(instance2);
}


According to the results, you can see that the singleton mode is broken again and two objects are created! How to solve this situation?

The traffic light method can be used to define whether a flag bit record object is created

package Singleton mode;

import java.lang.reflect.Constructor;

//Double detection lock
public class DoubleLock {
    private volatile static DoubleLock doubleLock;

    //Flag bit
    private static boolean flag = false;

    private DoubleLock() {
        synchronized (DoubleLock.class) {
            if (flag == false)
                flag = true;
            else
                throw new RuntimeException("Do not attempt to use reflection to break exceptions");
        }
        System.out.println("Create sample");
    }

    public static DoubleLock getInstance() {
        if (doubleLock == null) {
            synchronized (Lazy.class) {
                if (doubleLock == null)
                    doubleLock = new DoubleLock();
            }
        }
        return doubleLock;
    }

    public static void main(String[] args) throws Exception {
        Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        DoubleLock instance1 = constructor.newInstance();
        DoubleLock instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}


We can see that we have solved this problem again by setting the flag bit flag, but once the keyword is obtained, the singleton mode can still be cracked through reflection, as shown below

public static void main(String[] args) throws Exception {
    Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);
    Field declaredField = DoubleLock.class.getDeclaredField("flag");
    constructor.setAccessible(true);
    declaredField.setAccessible(true);
    DoubleLock instance1 = constructor.newInstance();
    declaredField.set(instance1, false);//After the first object is created, change the flag to false
    DoubleLock instance2 = constructor.newInstance();
    System.out.println(instance1);
    System.out.println(instance2);
}


It can be seen that the singleton mode is broken again; Therefore, in order to make the program more secure, the flag keyword is usually encrypted

So how to completely avoid reflection breaking the singleton mode? Let's check the source code of newInstance

You can see that if it is an enumeration type, you cannot obtain enumeration through reflection;

Therefore, the fifth singleton mode is introduced

5. Enumerate single examples

  • Advantages: simple implementation, enumeration itself is a singleton mode. The JVM provides guarantee fundamentally! Avoid vulnerabilities through reflection and deserialization!

  • Disadvantages: no delayed loading

package Singleton mode;

import java.lang.reflect.Constructor;

//enum is essentially a Class
public enum EnumSingle {
    INSTANCE;

    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

We create objects through reflection again, and report an error according to the result. There is no empty construction method of EnumSingle, which is not what we want to see


We decompile the class file of EnumSingle, and we can see the empty construction method

However, there is no parameterless structure in the implementation of explicit error reporting. We use a more professional decompile tool jad to decompile the class file

It can be seen that the enumeration Class essentially inherits the Enum Class. It is a Class in itself, and there is no parameterless construction, but a parameterless construction with two parameters. We modify the code to test

public static void main(String[] args) throws Exception {
    EnumSingle instance1 = EnumSingle.INSTANCE;
    Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
    declaredConstructor.setAccessible(true);
    EnumSingle instance2 = declaredConstructor.newInstance();
    System.out.println(instance1);
    System.out.println(instance2);
}


This correctly displays the error message: the enumeration object cannot be created reflectively

Topics: Java Design Pattern