Design pattern - singleton pattern

Posted by tzikis on Tue, 21 Sep 2021 13:27:19 +0200

Singleton mode

Singleton mode: the object of a class is obtained multiple times in the code. The results obtained multiple times are the same object. This object is a singleton object.

Common singleton mode

Since a singleton does not allow multiple duplicate objects, and generally objects are created through constructors, na then the singleton pattern must not allow users to call constructors.

Hungry Han style single case

The reason why it is called hungry man style is that this only instance will be created at the beginning. It seems very urgent. Like a hungry man, it can't stand to put his food away.

// Look at the red font and know that the constructor must be privatized
public class HungerySingleton {
    // Privatization construction method
    private HungerySingleton(){}
    
    private static final HungerySingleton instance = new HungerySingleton();
    
    public static HungerySingleton getInstance(){
        return instance;
    }
}

Static borrows the feature that static attributes will be loaded when Java classes are loaded, which perfectly solves the problem that the instance will only be loaded once. getInstance() provides an external interface to get an instance. The volatile keyword prevents instruction reordering from causing assignment failure. As for volatile, I will mention it later, and the portal will be completed at that time.
Under simple test:

public class HungryTest {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                HungerySingleton instance = HungerySingleton.getInstance();
                System.out.println(instance);
            }).start();
        }
    }
}

Output results:

hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2. 
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2
hungry.HungerySingleton@41924ee2

Twenty threads output the same results, so these twenty objects belong to the same object.

Lazy single case

When I was a child, I was found lazy by the old man. The old man told me a story. A man was too lazy to carry a bowl for dinner, so he hollowed out the inner ring of the pancake and hung it around his neck. When he was hungry, he bit his mouth. However, when the cake on his mouth was finished, he was too lazy to turn the cake around, and finally starved to death.
Lazy style is like summer vacation when you were a child. You must wait until you are almost happy to make up your homework. Do not create an instance when the class is loaded. Wait until it is used for initialization.

public class SimpleLazy {
    // Privatize the constructor so that users cannot create instances through the constructor
    private SimpleLazy(){}
    // No initialization
    private static SimpleLazy instance = null;
    // An interface provided to the outside world to obtain an instance
    public static SimpleLazy getInstance(){
        if(instance == null){
            instance = new SimpleLazy();
        }
        return instance;
    }
}

This is an imperfect singleton mode. In the case of single thread, singleton can be guaranteed. However, in the case of high concurrency, when two threads run to if(instance == null) at the same time, the judgment results of both threads are true. Therefore, two instances will appear, and the second instance will overwrite the first one. At this time, the single case has undoubtedly been destroyed.
Similarly, use the code of the test class above to make simple modifications:

public class SimpleLazyTest {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                SimpleLazy instance = SimpleLazy.getInstance();
                System.out.println(instance);
            }).start();
        }
    }
}

Final output result:
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@edfb547
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
lazy.SimpleLazy@20917dd4
The objects obtained by the second thread began to be dishonest. This is why non singletons began to appear in 20 threads. Such singletons cannot be used in all high concurrency cases.
In order to solve the lazy singleton under concurrency, locks can be introduced. Add an exclusive lock to the method to allow the thread accessing the method to queue for a single entry, so there is no chance to destroy the singleton.

public class SimpleLazy {
    // Privatize the constructor so that users cannot create instances through the constructor
    private SimpleLazy(){
    public class SimpleLazy {
    // Privatize the constructor so that users cannot create instances through the constructor
    private SimpleLazy() {
    }

    // No initialization
    private static SimpleLazy instance = null;

    // An interface provided to the outside world to obtain an instance
    public static SimpleLazy getInstance(){
    // public static synchronized SimpleLazy getInstance() {
        if (instance == null) {
            synchronized (SimpleLazy.class) {
                if(instance == null) {
                    instance = new SimpleLazy();
                }
            }
        }
        return instance;
    }
}
    }
    // No initialization
    private static SimpleLazy instance = null;
    // An interface provided to the outside world to obtain an instance
    // public static SimpleLazy getInstance(){
    public static synchronized SimpleLazy  getInstance(){
        if(instance == null){
            instance = new SimpleLazy();
        }
        return instance;
    }
}

The test code remains unchanged, and the results are obviously single instances that can ensure concurrency. The test results will not be posted here.

Double check lock lazy agent

The purpose of concurrency is to make full use of CPU performance. Locking limits thread parallelism, which runs counter to the original intention. However, performance and security always weigh the pros and cons. If the granularity of locking is further refined, it seems acceptable.

public class SimpleLazy {
    // Privatize the constructor so that users cannot create instances through the constructor
    private SimpleLazy() {
    public class SimpleLazy {
    // Privatize the constructor so that users cannot create instances through the constructor
    private SimpleLazy() {
    }

    // No initialization
    private static SimpleLazy instance = null;

    // An interface provided to the outside world to obtain an instance
    public static SimpleLazy getInstance(){
    // public static synchronized SimpleLazy getInstance() {
        if (instance == null) {
            synchronized (SimpleLazy.class) {
                if(instance == null) {
                    instance = new SimpleLazy();
                }
            }
        }
        return instance;
    }
}
    }

    // No initialization
    private static SimpleLazy instance = null;

    // An interface provided to the outside world to obtain an instance
    public static SimpleLazy getInstance(){
        if (instance == null) {
            synchronized (SimpleLazy.class) {
                if(instance == null) {
                    instance = new SimpleLazy();
                }
            }
        }
        return instance;
    }
}

What may be controversial in this paragraph is whether it is appropriate to remove the outermost if (instance == null). In fact, if the outer layer is removed, the performance will decrease with the increase of concurrency. On the one hand, after removing the judgment of the outer layer, the lock performance of the code is no different from that of the above method. On the other hand, the judgment of the outermost layer is that the request for the start of the second access is blocked, and the instance will be returned directly instead of determining whether to create the object after obtaining the lock again. The inner if (instance == null) is the key to solve high concurrency. The judgment of the outer layer is concurrent, so multiple threads may queue to obtain locks. Therefore, if there is no judgment of the inner layer, when the first thread releases the lock, the thread entering the first layer will directly recreate an object.

Single case of reflection destruction

If you are familiar with reflection, you will know that there are some methods in reflection, Class.getDeclare * * *, which is the method that can get private modification (fields, functions, constructors, etc.), and then there is a setAccessible method, which is a very violent method. You can forcibly modify the method and field settings of private modification. Therefore, get the constructor through reflection, and then set it to force modification, so as to create a new instance.

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // Get single instance normally
        HungerySingleton instance = HungerySingleton.getInstance();
        // Get construction method through reflection (including private modification)
        Constructor<HungerySingleton> cons = HungerySingleton.class.getDeclaredConstructor();
        // Setting forced access and modification
        cons.setAccessible(true);
        // Create a new instance through the constructor
        HungerySingleton consInstance = cons.newInstance();
        System.out.println(instance);
        System.out.println(consInstance);
        System.out.println(instance == consInstance);
    }

Output results:

hungry.HungerySingleton@4554617c
hungry.HungerySingleton@74a14482
false

From this point of view, is it unsafe for a single case of hungry Han style. Of courself not! Since reflection will think of attacking from the constructor, we will defend from the constructor.

public class HungerySingleton {
    // Privatization construction method
    private HungerySingleton(){
        throw new RuntimeException("It is forbidden to create singletons through constructors Bean,Please respect yourself!");
    }

    private volatile static HungerySingleton instance = new HungerySingleton();

    public static HungerySingleton getInstance(){
        return instance;
    }
}

Prevent users from forcibly creating instances through reflection by blocking constructors.

Serialization destruction singleton

public class HungerySingleton implements Serializable {
    private HungerySingleton(){
   		throw new RuntimeException("It is forbidden to create singletons through constructors Bean,Please respect yourself!");
    }
    private static final HungerySingleton instance = new HungerySingleton();
    public static HungerySingleton getInstance(){
        return instance;
    }
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
    // Serialize the instance of the singleton to a file
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("tempFile"));
    HungerySingleton dc = HungerySingleton.getInstance();
    objectOutputStream.writeObject(dc);
    // Deserialize from file to instance
    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("tempFile"));
    HungerySingleton o = (HungerySingleton)objectInputStream.readObject();
    System.out.println(o==dc);
}

Output results:

false

It seems difficult to solve this problem, but it's not difficult for us to think about deserialization, so we have to look at the source code here.
If you are not familiar with the way to look at the source code, you can look at it through Debug. If you are an old driver, follow it with your feelings. Don't lose big for small, don't stick to details. Due to space limitations, the call stack is posted here directly to the key code.

The highlighted line on a black background indicates that other methods will be called here.

Here, stare at

Object obj;
try {// Judge whether the construction method is empty (it must not be empty), so it must receive a new object with obj
    obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
    throw (IOException) new InvalidClassException(
        desc.forClass().getName(),
        "unable to create instance").initCause(ex);
}

Where the ternary expression obj = desc.isinstantable()? desc.newInstance() : null; This means that when desc.isinstantable() returns true, desc.newinstance() will be performed, otherwise null will be returned. So turn your attention to the isinstiabl () method.

boolean isInstantiable() {
    requireInitialized();
    return (cons != null);
}

Mainly judge whether cons (cons is the constructor private constructor <? > cons;) is empty. This is a class. Of course, the constructor will not be null, so it must execute the desc.newInstance() method (the core content of this method is return cons.newInstance();, which is created through the construction method). Therefore, after deserialization, a new object is created.
Moreover, it cannot be prevented by limiting the constructor, because if the constructor does not execute successfully, the deserialization fails.

Look at this Code:

if (obj != null &&
    handles.lookupException(passHandle) == null &&
    desc.hasReadResolveMethod())
{// Call the readResolve method to get an object
    Object rep = desc.invokeReadResolve(obj);
    if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
    }
    if (rep != obj) {
        // Filter the replacement object
        if (rep != null) {
            if (rep.getClass().isArray()) {
                filterCheck(rep.getClass(), Array.getLength(rep));
            } else {
                filterCheck(rep.getClass(), -1);
            }
        }
        handles.setObject(passHandle, obj = rep);
    }
}

return obj;

Although obj (the instance created through the constructor) is returned in the end, you can see that there is a function handles.setObject(passHandle, obj = rep);, From the naming of the function, it seems that it is meaningful to set a value, and the second parameter actually assigns rep to obj.
If this method can guarantee certain execution, there will only be one instance of name. So look at the conditions above to enter this part of the code.

if (obj != null &&
    handles.lookupException(passHandle) == null &&
    desc.hasReadResolveMethod())
{

handles.lookupException(passHandle) == null. You can ignore it. Just look at desc.hasReadResolveMethod())

/**If the current class can be serialized or externalizable (also the interface that implements serialization), and the readResolve method is defined, it returns true; otherwise, it returns false
* Returns true if represented class is serializable or externalizable and
* defines a conformant readResolve method.  Otherwise, returns false.
*/
boolean hasReadResolveMethod() {
   requireInitialized();
   return (readResolveMethod != null);
}

readResolveMethod is the private Method readResolveMethod; This means that if there is this method in the serialized and deserialized class, it will be like the code block we expect.

But what exactly does this method need to do? In addition, even if only one instance is returned in the end, how can we ensure that the returned instance before and after serialization is the same?

This is the problem we threw out. Now that the problem is thrown out, our goal is more clear. First, we should define this method; Second: ensure that the instances before serialization and after deserialization are the same.
So he boldly wrote:

public Object readResolve(){
    return instance;
}

Using the original test method, I didn't roll over.
Therefore, for singleton mode, if you want to prevent reflection from breaking singleton, throw an exception in the constructor. To prevent serialization and deserialization from destroying singletons, write the readResolve method. Of course, adults are All In. These two guarantees are added.

Anonymous inner class singleton

Only when the external method is called, the logic of the internal class will be executed. Therefore, the underlying execution logic of the JVM is used to perfectly avoid the thread safety problem

public class InternalSingleton {
    private InternalSingleton(){
        if(InnerClass.instance != null){
            throw new RuntimeException("Please pass getInstance Method to get an instance!");
        }
    }

    public static InternalSingleton getInstance(){
        return InnerClass.instance;
    }

    private static class InnerClass{
        private static final InternalSingleton instance = new InternalSingleton();
    }
}

It should be noted that the singleton instance can only be instantiated by calling the constructor of the external class through the internal class. Because the instance is placed in the internal column and modified by final, it will only be instantiated once. The constructor of an external class should be called normally for the first time and not allowed for the second time, so judgment is added.
test

public class InternalSingletonTest {
    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                InternalSingleton instance = InternalSingleton.getInstance();
                System.out.println(instance);
            }).start();
        }
    }
}

The test results will not be posted, and there is still a single case under multithreading.

Container single example

The singleton instance can be registered by storing non repeatable data and supporting concurrent collections, so as to realize the singleton.

public class ContainnerSingleton {
    // Concurrent container support
    private static ConcurrentHashMap<String,ContainnerSingleton> container = new ConcurrentHashMap<>();
    // Privatization construction method
    private ContainnerSingleton(){
        if(container.size() > 0){
            throw new RuntimeException("It is forbidden to create an instance through the construction method!");
        }
    }

    public static ContainnerSingleton getInstance(String url) {
        // '' and null are not allowed as keys
        if(null == url ||"".equals(url)){
            return null;
        }else if(!container.containsKey(url)){
            // The old object is created when it does not exist
            // Use the putIfAbsent method of the concurrent container to prevent repeated additions
            container.putIfAbsent(url,new ContainnerSingleton());
        }
        return container.get(url);
    }
}

Because the putIfAbsent method exists, objects with the same key in the container cannot be overwritten. Adding validation to the constructor only prevents reflection from directly creating instances. At the same time, you can also add the readResolve method to prevent serialization from destroying singletons.

For yourself

From the bottom of my heart, I am not familiar with enumeration at present, because it is rarely used. In fact, enumeration can also implement singleton, and the singleton implemented by enumeration cannot be destroyed by serialization. In this part, first set up a flag for yourself, and then update the enumeration implementation singleton, and fully analyze why serialization and deserialization cannot destroy the enumeration singleton.

Time: 23:35:30, September 20, 2021

Topics: Java Design Pattern