Do you really use the singleton mode?

Keywords: Java jvm github

Singleton mode can be written as long as it is a qualified development, but if you want to go deeper, a small singleton mode can involve many things, such as: Is multithreading safe?Is it lazy to load?Performance, etc.Do you know how to write several singleton modes?How do I prevent reflection from destroying the singleton mode?

1. Single Case Mode

1.1 Definition

The singleton mode is to create a globally unique object by instantiating it only once while the program is running.It's a bit like Java's static variables, but the singleton pattern is better than static variables:

  1. Static variables are loaded by the JVM at program startup, which can cause a lot of waste of resources if not used.
  2. Singleton mode enables lazy loading and allows you to create instances only when you are using them.

Many of the tool classes in the Development Tools Class Library use the singleton mode, such as proportional thread pools, caches, log objects, etc. All they need to do is create one object. If multiple instances are created, unpredictable problems may arise, such as waste of resources, inconsistent processing of results, and so on.

1.2 Single Case Realization Thought

  1. Static instance object;
  2. Private construction methods, prohibit instance creation through construction methods;
  3. Provides a public static method for returning a unique instance.

Benefits of 1.3 Singletons

  1. There is only one object, low memory cost, good performance;
  2. Avoid multiple occupation of resources;
  3. Set up global access points in the system to optimize and share resource access.

2. Realization of singleton mode

  1. Hungry Han Mode
  2. Lazy Man Mode
  3. Double check lock mode
  4. Static Internal Class Singleton Mode
  5. Enumeration classes implement singleton mode

2.1 Hungry Han Mode

Objects are instantiated directly when defining static properties

public class HungryMode {

    /**
     * Use static variables to store unique instances
     */
    private static final HungryMode instance = new HungryMode();

    /**
     * Privatization constructor
     */
    private HungryMode(){
        // There can be many operations inside
    }

    /**
     * Provide a public access instance interface
     * @return
     */
    public static HungryMode getInstance(){
        return instance;
    }
}

2.1.1 Advantages

The use of the static keyword ensures that when referencing this variable, all writes to it are done, thus ensuring JVM-level thread security

2.1.2 Disadvantages

Lazy loading is not possible and wastes space: if a class is large, we load it at initialization, but we haven't used it for a long time, which results in a waste of memory space.

So, can you initialize a singleton class only if the getInstance() method is used, and then load the data in the singleton class?So there's it: the lazy style.

2.2 Lazy Mode

Lazy mode is a lazy mode. Instances are not created when programs are initialized, but only when instances are used. Therefore, lazy mode solves the space waste caused by hungry mode.

2.2.1 General implementation of the lazy mode

public class LazyMode {
    /**
     * Instance not initialized when defining static variables
     */
    private static LazyMode instance;

    /**
     * Privatization constructor
     */
    private LazyMode(){
        // There can be many operations inside
    }
    /**
     * Provide a public access instance interface
     * @return
     */
    public static LazyMode getInstance(){
        // When used, first determine if the instance is empty, and if it is empty, instantiate the object
        if (instance == null) {
            instance = new LazyMode();
        }
        return instance;
    }
}

However, this implementation is not secure in a multi-threaded scenario, where multiple instances may occur:

if (instance == null) {
    instance = new LazyMode();
}

Suppose two threads enter this code at the same time, because there is no resource protection, so both threads can determine that instances are empty at the same time, and both will initialize instances, so multiple instances will occur.

2.2.2 Lazy Mode Optimization

We add the synchronized keyword to the getInstance() method so that it can solve the problem of multiple instances if it becomes a protected resource.

public class LazyModeSynchronized {
    /**
     * Instance not initialized when defining static variables
     */
    private static LazyModeSynchronized instance;
    /**
     * Privatization constructor
     */
    private LazyModeSynchronized(){
        // There can be many operations inside
    }
    /**
     * Provide a public access instance interface
     * @return
     */
    public synchronized static LazyModeSynchronized getInstance(){
        /**
         * Class class locks are added, affecting performance, and the code is serialized after locking.
         * Most of our code blocks are read operations, so the code threads are secure with read operations
         *
         */
        if (instance == null) {
            instance = new LazyModeSynchronized();
        }
        return instance;
    }
}

2.2.3 Advantages of the Lazy Mode

Lazy loading is achieved, saving memory space.

2.2.4 Disadvantages of the Lazy Mode

  1. Threads are insecure without locking and multiple instances may occur.
  2. In the case of locks, serialization of programs can result in serious performance problems for the system.

Locking in lazy mode. For the getInstance() method, most operations are read operations. Read operations are thread-safe, so we don't have to have locks on each thread to invoke this method. We need to adjust the locking problem.This also gives rise to a new implementation mode: the double-check lock mode.

2.3 Double Check Lock Mode

General implementation of 2.3.1 double check lock mode

public class DoubleCheckLockMode {

    private static DoubleCheckLockMode instance;

    /**
     * Privatization constructor
     */
    private DoubleCheckLockMode(){

    }
    /**
     * Provide a public access instance interface
     * @return
     */
    public static DoubleCheckLockMode getInstance(){
        // First judgment, if this is empty, don't go into the lock-grab phase and return to the instance directly
        if (instance == null) {
            synchronized (DoubleCheckLockMode.class) {
                // Judge again if it is empty after grabbing the lock
                if (instance == null) {
                    instance = new DoubleCheckLockMode();
                }
            }
        }
        return instance;
    }
}

The double-checked lock mode solves the singleton, performance, and thread security issues, but it also has problems with this writing: null pointer problems may occur in multi-threaded situations because JVM optimizes and reorders instructions when instantiating objects.

2.3.2 What is instruction rearrangement?

private SingletonObject(){
      // Step One
     int x = 10;
      // Step 2
     int y = 30;
     // Step 3
     Object o = new Object(); 
}

The above constructor, SingletonObject(), is reordered by the JVM, so the execution order may be messy, but regardless of the execution order, the JVM will guarantee that all instances will be instantiated.If there are many operations in a constructor, the JVM returns an object if the properties inside the constructor have not been fully instantiated in order to increase efficiency.The reason for the null pointer problem with double-check locks is that it occurs here. When a thread acquires a lock for instantiation, the other threads get the instance directly for use. Because of the JVM reordering, the object acquired by the other threads may not be a complete object, so it occurs when the instance is usedNull pointer exception problem.

2.3.3 Double Check Lock Mode Optimization

To resolve the null pointer exception caused by double-check lock mode, you only need to use the volatile keyword, which strictly follows the happens-before principle, that is, the write operation must be completed before the read operation.

public class DoubleCheckLockModelVolatile {
    /**
     * Add the volatile keyword to ensure that the write operation must be completed before the read operation
     */
    private static volatile DoubleCheckLockModelVolatile instance;
    /**
     * Privatization constructor
     */
    private DoubleCheckLockModelVolatile(){

    }
    /**
     * Provide a public access instance interface
     * @return
     */
    public static DoubleCheckLockModelVolatile getInstance(){

        if (instance == null) {
            synchronized (DoubleCheckLockModelVolatile.class) {
                if (instance == null) {
                    instance = new DoubleCheckLockModelVolatile();
                }
            }
        }
        return instance;
    }
}

2.4 Static Internal Class Mode

Static internal class mode is also known as single holder mode. Instances are created by internal classes. Since the JVM does not load static internal classes during the loading of external classes, only the properties/methods of internal classes are loaded when they are called and their static properties are initialized.Static attributes are decorated with statics to ensure that they are instantiated only once and that the order of instantiation is strictly guaranteed.

public class StaticInnerClassMode {

    private StaticInnerClassMode(){

    }

    /**
     * Single Holder
     */
    private static class InstanceHolder{
        private  final static StaticInnerClassMode instance = new StaticInnerClassMode();

    }

    /**
     * Provide a public access instance interface
     * @return
     */
    public static StaticInnerClassMode getInstance(){
        // Call internal class properties
        return InstanceHolder.instance;
    }
}

This way is similar to but different from the Hungry-Han way.Both employ a class loading mechanism to ensure that there is only one thread initializing the instance.Different places:

  1. The Hungry Han style is instantiated as long as the Singleton class is loaded, without the effect of Lazy-Loading.
  2. Static internal class methods do not instantiate when the Singleton class is loaded, but when instantiation is required, call the getInstance() method to load the SingletonInstance class to complete the Singleton instantiation.

The static properties of a class are only initialized when it is first loaded, so here, the JVM helps us keep threads safe, and no other threads can enter when the class is initialized.

This way, without any locks, ensures security under multiple threads without any performance impact or waste of space.

2.5 Enumeration classes implement singleton mode

Because enumeration types are thread-safe and can only be loaded once, designers take full advantage of this feature of enumeration to implement the singleton mode. Enumeration is written simply, and enumeration types are the only singleton implementation mode used that will not be destroyed.

public class EnumerationMode {
    
    private EnumerationMode(){
        
    }

    /**
     * Enumeration types are thread-safe and can only be loaded once
     */
    private enum Singleton{
        INSTANCE;

        private final EnumerationMode instance;

        Singleton(){
            instance = new EnumerationMode();
        }

        private EnumerationMode getInstance(){
            return instance;
        }
    }

    public static EnumerationMode getInstance(){

        return Singleton.INSTANCE.getInstance();
    }
}

Where applicable:

  1. Objects that need to be created and destroyed frequently;
  2. Objects that take too much time or resources to create but are often used;
  3. Tool class object;
  4. Objects that frequently access databases or files.

3. Problems and Solutions of the Single Case Model

Except enumeration, all other methods destroy the singleton by reflection

3.1 Disruption of Singleton Mode

/**
 * Examples of static internal class implementations
 * @throws Exception
 */
@Test
public void singletonTest() throws Exception {
    Constructor constructor = StaticInnerClassMode.class.getDeclaredConstructor();
    constructor.setAccessible(true);

    StaticInnerClassMode obj1 = StaticInnerClassMode.getInstance();
    StaticInnerClassMode obj2 = StaticInnerClassMode.getInstance();
    StaticInnerClassMode obj3 = (StaticInnerClassMode) constructor.newInstance();

    System.out.println("The output is:"+obj1.hashCode()+"," +obj2.hashCode()+","+obj3.hashCode());
}

Console Printing:

The output is 1454171136,1454171136,1195396074

From the output, we can see that obj1 and obj2 are the same object, and obj3 is the new object.Obj3 is a reflection mechanism that allows us to call a private constructor and produce a new object.

3.2 How to Prevent Single Damage

You can make judgments in the construction method and prevent new instances from being generated if there are already instances. The solution is as follows:

public class StaticInnerClassModeProtection {

    private static boolean flag = false;

    private StaticInnerClassModeProtection(){
        synchronized(StaticInnerClassModeProtection.class){
            if(flag == false){
                flag = true;
            }else {
                throw new RuntimeException("Instance already exists, please pass getInstance()Method acquisition!");
            }
        }
    }

    /**
     * Single Holder
     */
    private static class InstanceHolder{
        private  final static StaticInnerClassModeProtection instance = new StaticInnerClassModeProtection();
    }

    /**
     * Provide a public access instance interface
     * @return
     */
    public static StaticInnerClassModeProtection getInstance(){
        // Call internal class properties
        return InstanceHolder.instance;
    }
}

Test:

/**
 * Judges in the construction method, throws RuntimeException if present
 * @throws Exception
 */
@Test
public void destroyTest() throws Exception {
    Constructor constructor = StaticInnerClassModeProtection.class.getDeclaredConstructor();
    constructor.setAccessible(true);

    StaticInnerClassModeProtection obj1 = StaticInnerClassModeProtection.getInstance();
    StaticInnerClassModeProtection obj2 = StaticInnerClassModeProtection.getInstance();
    StaticInnerClassModeProtection obj3 = (StaticInnerClassModeProtection) constructor.newInstance();

    System.out.println("The output is:"+obj1.hashCode()+"," +obj2.hashCode()+","+obj3.hashCode());
}

Console Printing:

Caused by: java.lang.RuntimeException: Instance already exists, please pass getInstance()Method acquisition!
    at cn.van.singleton.demo.mode.StaticInnerClassModeProtection.<init>(StaticInnerClassModeProtection.java:22)
    ... 35 more

4. Summary

4.1 Comparison of various implementations

Name Hungry Han Mode Lazy Man Mode Double check lock mode Static internal class implementation Enumeration Class Implementation
Usability available Not recommended Recommended Use Recommended Use Recommended Use
Characteristic Lazy loading is not possible and may result in a waste of space Unlocked threads are insecure; poor locking performance Thread security; Delayed loading; High efficiency Avoid thread insecurity, delay loading, and high efficiency. Simple writing; thread-safe; load only once

4.2 Sample Code Address

Github sample code

Posted by php_blob on Sun, 01 Sep 2019 10:41:33 -0700