Talk about the most difficult design pattern - singleton pattern

Many people are sure to be confused, because in your impression, the singleton mode is still very simple to implement. Don't worry. Look down slowly and you'll see why I said it was the hardest.

1. Basic concepts

  • Singleton pattern is a commonly used creative design pattern. The singleton pattern ensures that the class has only one instance and provides a global access point.

2. Applicable scenarios

  • To ensure that there is absolutely only one instance in any case.

  • Typical scenarios are: windows Task Manager, windows Recycling Station, Thread Pool Design, etc.

3. Advantages and disadvantages of the singleton model

Advantage

  • There is only one instance in memory, which reduces memory overhead.
  • It can avoid multiple occupancy of resources.
  • Set up global access points and strictly control access.

shortcoming

  • Without interfaces, expansion is difficult.

4. Common implementation patterns

  • Slacker type
  • Hungry man

5. Have a lazy play first.

public class LazySingleton {
    // 1. Private Objects
    private static LazySingleton lazySingleton = null;

    // 2. Privatization of construction methods
    private LazySingleton() {}

    // 3. Setting Global Access Points
    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

Next, let's do a single-threaded test

public class MainTest {
    public static void main(String[] args) {
        LazySingleton instance = LazySingleton.getInstance();
        LazySingleton instance2 = LazySingleton.getInstance();
        System.out.println(instance == instance2);
    }
}

  • The test code and results are as above. Everything looks and feels the same.

Naturally, let's consider multithreading.

  • Let's create a thread class
public class MyThread implements Runnable {
    @Override
    public void run() {
        LazySingleton instance = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + " " + instance);
    }
}
  • Then modify our test code
public class MainTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyThread());
        Thread t2 = new Thread(new MyThread());
        t1.start();
        t2.start();
        System.out.println("program end.");
    }
}
  • We tested the problem under multithreading by using the breakpoint test that comes with IDEA. We interrupted the breakpoint at Lazy Singleton as follows. (Set the breakpoint Suspend to Thread)

  • We debug the test code, and then switch threads through IDEA's tool window for viewing. (Specific IDEA debugging multi-threaded code method can be learned through various ways, of course, you can also find me, I teach you. Although I know a little about it.

  • At this point, you see Thread-0 and Thread-1 threads. Both threads judge lazy Singleton to be empty, and both threads create objects.

  • After executing the code, you can see the message printed by the console.

  • Obviously, the two threads get different objects. It also shows that our lazy code as above is not thread-safe and may create multiple objects under multi-threading.

Next, we should find a way to deal with this situation.

  • By adding synchronized keyword processing to global access points
// 3. Setting Global Access Points
public synchronized static LazySingleton getInstance() {
    if (lazySingleton == null) {
        lazySingleton = new LazySingleton();
    }
    return lazySingleton;
}

The above problem has been solved, but a new problem has arisen. This method will lock when accessing, which will reduce the efficiency of accessing. However, as long as the object is judged and created, it can be locked. Probably, the object has been created, and concurrent access is not a problem. In order to achieve this goal, we put forward "Double Check Double Check Double Check" scheme.

  • Don't talk too much nonsense. Code it.
public class LazyDoubleCheckSingleton {
    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton;

    private LazyDoubleCheckSingleton() {}

    public static LazyDoubleCheckSingleton getInstance() {
        if (lazyDoubleCheckSingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazyDoubleCheckSingleton == null) {
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}
  • This code implements that lazy Double Check Singleton is no longer empty in the case of high probability, and it does not need to acquire locks, so it can achieve multi-threaded concurrent access.

But there are still some problems with the above code, because the problem is difficult to reproduce, so we will not do the demonstration. The problem is caused by the well-known "instruction reordering".

  • To outline the principle, may not be very accurate, but the main purpose is to understand the problem.

  • Actually, creating an object (new Lazy Double CheckSingleton ()) is an operation that we can see at the bottom as three steps:

    • Memor = allocate (); // 1: Allocate memory space for objects
    • CtorInstance (memory); //2: Initialization object
    • Lazy DoubleCheckSingleton = memory; //3: Set lazy DoubleCheckSingleton to point to the memory address just allocated
  • To solve this problem, the Java language specification requires that intra-thread semantics (intra-thread semantics) be complied with to ensure that reordering does not change the results of program execution in a single thread.

  • But in the above example, the reordering may occur in steps 2 and 3, that is, it may occur, pointing to the memory address first, then initializing the object. At this time, lazy Double Check Singleton is not empty, but the object has not been initialized yet. The problem arises. And the reordering operation does not violate intra-thread semantics at this time, because such reordering will not affect the final result in a single thread.

Let's illustrate the problem caused by instruction reordering in the previous figure.

  • This happens when the object in thread 0 is not initialized and thread 1 accesses the object.

When that problem comes, it's time to deal with it.

  • In view of the above problems, there are actually two ways to deal with them:
    • Steps 2 and 3 are not allowed to be reordered.
    • Steps 2 and 3 are allowed to be reordered, but this reordering process is not visible to other threads.

No reordering of steps 2 and 3 is allowed

  • Just add volatile keywords to the object.
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton;
  • The principle of the specific analysis will be carried out in other content, not the focus of this time.

Steps 2 and 3 are allowed to be reordered, but this reordering process is not visible to other threads.

Solutions Based on Static Internal Classes

public class StaticInnerClassSingleton {
    // InnerClass Object Lock
    private static class InnerClass {
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }

    private StaticInnerClassSingleton() {}

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.staticInnerClassSingleton;
    }
}

So far, let's finish our lazy style. Lost heart crazy ah, there is wood....

6. Let's play Hungry Han Style again.

public class HungrySingleton {
    private final static HungrySingleton hungrySingleton;

    static {
        hungrySingleton = new HungrySingleton();
    }

    private HungrySingleton() {}

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}
  • This thing is better in multi-threading, because the hungry Chinese style creates the object when the class is initialized, so it does not need to judge whether the object is empty, of course, there is not so much to consider in multi-threading.

7. Then, let's see if there's anything wrong with the singleton pattern in the case of serialization and deserialization.

  • Because the serialization problem has nothing to do with lazy or hungry Chinese implementation, the following is an example of hungry Chinese code.

Hungry man

  • First, our singleton class implements serialization
public class HungrySingleton implements Serializable {
    // ...
}
  • Then let's write a test code.
public class MainTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.txt"));
        oos.writeObject(instance);

        File file = new File("test.txt");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));

        HungrySingleton newInstance = (HungrySingleton) ois.readObject();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}
  • Let's take a look at the implementation results.

  • Ha ha ha ha, instantly suffocated, there are wood...

In view of the above problems, let's take a look at the source code and find out the reasons.

  • Following is the process of tracking the source code. I only do simple screenshots. I am interested in researching it by myself. (Haha, or you can find me, let's study it together.)

Seeing this, I think you should also know that the desc.isInstantiable() method returns true, so by reflecting a new object, the read object is not the same as the written object.

Then you must want to ask me, how to deal with it, don't worry, and then look down.

  • The initialization of this variable can be seen directly through the lookup.

That's not clear. When we have readResolve() method, we return the singleton object directly by calling this method. Then we can simply add a method to our singleton class.

private Object readResolve() {
    return hungrySingleton;
}
  • Then you re-test the code directly, and the results are as follows.

8. Serialization and serialization are over. Let's look at reflection again. After all, reflection is still used a lot. It is also a common operation to create an object by reflection.

This problem is different for the two ways. Let's first look at the performance of hungry Chinese.

  • Let's write a test code.
public class MainTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass = HungrySingleton.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}
  • Look at the results.

There is a feeling of five thunders, don't worry, don't worry, let's do it slowly, although it takes some time, but can get a lot of things.

Now that the problem arises, how to deal with it? In fact, the processing is also simple, because reflection means that the privately constructed method permissions are open, so we can add judgment to the privately constructed method.

private HungrySingleton() {
    if (hungrySingleton != null) {
        throw new RuntimeException("Singleton constructor prohibits reflection calls!");
    }
}
  • Running our test code again shows that the following exception will be thrown.

Next, let's analyze the slacker style.

  • The same operation as hungry Chinese style additions can not avoid reflection.
  • If you first use the getInstance() method to get the object, and then use reflection to create the object, you can throw an exception.
  • But when we first use reflection to create objects and then use getInstance() method to get objects, we can get two different objects, or we can not avoid the destruction of the singleton pattern.

Finally, the conclusion is that lazy people can't prevent reflex attacks.

9. Then you're going to faint. You must want to ask if there are so many problems to consider when you make a single case in the future. It's too ink. Let's look at how to implement singletons with enumerations.

  • This method is recommended in the Effective Java book.
  • This method perfectly solves the damage of serialization and reflection to the singleton pattern.

Upper code

public enum EnumInstance {
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumInstance getInstance() {
        return INSTANCE;
    }
}

As mentioned above, the destruction of the singleton pattern by serialization and reflection is perfectly solved, so let's see how to solve it.

Resolve the damage of serialization to singleton patterns

  • Let's also look at the ObjectInputStream.readObject() method

  • You can see that Enum is retrieved by name reflection, and no new object is created, so the same object is retrieved.

Resolve the damage of reflection to singleton mode

  • To write a test code
public class MainTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objectClass = EnumInstance.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        EnumInstance instance = EnumInstance.getInstance();
        EnumInstance newInstance = (EnumInstance) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}
  • Result

  • Looking at the java.lang.Enum class, we can see that there is only one constructor and two parameters are required.

  • So let's pass in two parameters and try.

  • Final results

  • Let's take a look at the reason. See the constructor.newInstance() method.

  • It was found that it handled the Enum type and did not allow the creation of Enum objects by reflection.

  • At this point, we also understand why Enum singletons perfectly prevent serialization and reflection from destroying the singleton pattern.

OK, let's do two more things about it.

10. Let's talk about the container list.

  • For convenience, use HashMap to implement a container singleton

Go straight to the code

public class ContainerSingleton {
    private static Map<String, Object> singletonMap = new HashMap<>();

    private ContainerSingleton() {}

    public static void putInstance(String key, Object instance) {
        if (key != null && !"".equals(key) && instance != null) {
            if (!singletonMap.containsKey(key)) {
                singletonMap.put(key, instance);
            }
        }
    }

    public static Object getInstance(String key) {
        return singletonMap.get(key);
    }
}

Notes for the above code

  • Because their key s are the same, they eventually get the same object.

  • But the above code is thread insecure. In the case of multi-threading, if two threads simultaneously determine if condition is valid, then t1 thread put, t1 thread get; then t2 thread put, t2 thread get, t1 thread and t2 thread get different objects.

  • If the container singleton does not use HashMap at this time, using HashTable can achieve thread safety, but considering performance, if get requests are large, HashTable efficiency will be very low.

11. Finally, let's see how the ThreadLocal thread singleton is implemented.

Define a thread singleton class

public class ThreadLocalInstance {
    private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal =
            new ThreadLocal<ThreadLocalInstance>() {
                @Override
                protected ThreadLocalInstance initialValue() {
                    return new ThreadLocalInstance();
                }
            };

    private ThreadLocalInstance() {}

    public static ThreadLocalInstance getInstance() {
        return threadLocalInstanceThreadLocal.get();
    }
}

Implementing a Thread Class for Testing

public class MyThread implements Runnable {
    @Override
    public void run() {
        ThreadLocalInstance instance = ThreadLocalInstance.getInstance();
        System.out.println(Thread.currentThread().getName() + " " + instance);
    }
}

Write a test code to test it.

public class MainTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        System.out.println(Thread.currentThread().getName() + " " + ThreadLocalInstance.getInstance());
        System.out.println(Thread.currentThread().getName() + " " + ThreadLocalInstance.getInstance());
        System.out.println(Thread.currentThread().getName() + " " + ThreadLocalInstance.getInstance());
        Thread t1 = new Thread(new MyThread());
        Thread t2 = new Thread(new MyThread());
        t1.start();
        t2.start();
        System.out.println("program end.");
    }
}

Result

Our discussion today is now over. Today we mainly discuss the following contents.

  • The realization of basic singleton mode: lazy and hungry.
  • Discussions on thread security of single-case mode under multi-threading.
  • The destruction of the singleton pattern by serialization and deserialization.
  • Reflection destroys the singleton mode.
  • Enum enumerates singletons.
  • Singleton container.
  • ThreadLocal thread singleton.

Friends, come on!!!

Keywords: Java Windows

Added by Siann Beck on Wed, 11 Sep 2019 14:54:00 +0300