Singleton mode is really not simple

1, Foreword

Single case mode will face many problems whether in our interview or in our daily work. However, the details of many singleton patterns are worth exploring in depth.
This article connects various basic knowledge through single case mode, which is very worth reading.

1. What is singleton mode?

Singleton pattern is a very common software design pattern. It defines that the class of singleton object can only allow one instance to exist.

This class is responsible for creating its own objects and ensuring that only one object is created. It is generally used in business scenarios where resource consumption is required for the implementation of tool classes or the creation of objects.

Features of singleton mode:

  • Class constructor private
  • Hold a reference to your own class
  • Provide static methods for obtaining instances externally

Let's use a simple example to understand the usage of a singleton pattern.

public class SimpleSingleton {
    //Hold a reference to your own class
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();

    //Private construction method
    private SimpleSingleton() {
    }
    //Provide static methods for obtaining instances externally
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
    
    public static void main(String[] args) {
        System.out.println(SimpleSingleton.getInstance().hashCode());
        System.out.println(SimpleSingleton.getInstance().hashCode());
    }
}

Print results:

1639705018
1639705018

We see that the hashCode of the SimpleSingleton instance obtained twice is the same, indicating that the same object is obtained in the two calls.

Maybe many friends usually use it in their work, but I want to say that there is a problem with this code. Will you believe it?

No, let's look down together.

2, Hungry and lazy model

When introducing singleton mode, we must first introduce its two very famous implementation modes: hungry man mode and lazy man mode.

1. Hungry man model

The instance has been built during initialization. Whether you use it or not, build it first. The specific codes are as follows:

public class SimpleSingleton {
    //Hold a reference to your own class
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();

    //Private construction method
    private SimpleSingleton() {
    }
    //Provide static methods for obtaining instances externally
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
}

In fact, there is another variant of the hungry man model:

public class SimpleSingleton {
    //Hold a reference to your own class
    private static final SimpleSingleton INSTANCE;
    static {
       INSTANCE = new SimpleSingleton();
    }

    //Private construction method
    private SimpleSingleton() {
    }
    //Provide static methods for obtaining instances externally
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
}

Instantiate the INSTANCE object using a static code block.

The advantage of using hungry man mode is that there is no thread safety problem, but the disadvantage is also obvious.

Instantiate the object at the beginning. If the instantiation process is very time-consuming and the object is not used in the end, isn't it a waste of resources?

At this time, you may think that you don't need to instantiate the object in advance. Can you instantiate it when you really use it?

This is what I want to introduce next: lazy mode.

2. Lazy mode

As the name suggests, an instance is created only when it is used. It is "lazy". When it is used, it is checked whether there is an instance. If there is an instance, it will be returned. If there is no instance, it will be created. The specific codes are as follows:

public class SimpleSingleton2 {

    private static SimpleSingleton2 INSTANCE;

    private SimpleSingleton2() {
    }

    public static SimpleSingleton2 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SimpleSingleton2();
        }
        return INSTANCE;
    }
}

The INSTANCE object in the example is empty at first, and will not be instantiated until the getInstance method is called.
Well, good. But there is still a problem with this code.

3. synchronized keyword

What's wrong with the code above?

A: if the getInstance method is called in multiple threads, it may be true at the same time when the if (INSTANCE == null) judgment is reached, because the default value during INSTANCE initialization is null. This will lead to the creation of INSTANCE objects in multiple threads at the same time, that is, the INSTANCE object has been created multiple times, which is contrary to the original intention of creating only one INSTANCE object.

So, how to improve?

A: the easiest way is to use the synchronized keyword.

The improved code is as follows:

public class SimpleSingleton3 {
    private static SimpleSingleton3 INSTANCE;

    private SimpleSingleton3() {
    }

    public synchronized static SimpleSingleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SimpleSingleton3();
        }
        return INSTANCE;
    }
    public static void main(String[] args) {
        System.out.println(SimpleSingleton3.getInstance().hashCode());
        System.out.println(SimpleSingleton3.getInstance().hashCode());
    }
}

Add the synchronized keyword to the getInstance method to ensure that only one thread can create an INSTANCE of the INSTANCE object in the case of concurrency. Is that all right?

A: sorry, there is still a problem.

What's the problem?

A: using the synchronized keyword will consume the performance of the getInstance method. We should judge that the lock should be added only when the INSTANCE is empty. If it is not empty, the lock should not be added and we need to return directly.
This requires the use of the double check lock described below.

4. The difference between the pattern of hungry and lazy

but, before introducing the double check lock, let's insert a topic that friends may be more concerned about: what are the advantages and disadvantages of the hungry man mode and the lazy man mode?

Hungry man mode: the advantage is that there is no thread safety problem, and the disadvantage is a waste of memory space.
Lazy mode: the advantage is that there is no waste of memory space. The disadvantage is that if the control is not good, it is not a single case.

Well, let's take a safe look at how the double check lock ensures performance and single instance.


3, Double check lock

Double check lock, as its name implies, checks twice: check whether it is empty before locking, and check whether it is empty again after locking.

So, how does it implement singletons?

1. How to implement singleton?

The specific codes are as follows:

public class SimpleSingleton4 {

    private static SimpleSingleton4 INSTANCE;

    private SimpleSingleton4() {
    }

    public static SimpleSingleton4 getInstance() {
        if (INSTANCE == null) {
            synchronized (SimpleSingleton4.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SimpleSingleton4();
                }
            }
        }
        return INSTANCE;
    }
}

Judge whether it is empty before locking to ensure that if the INSTANCE is not empty, you can return directly without locking. Why do you need to judge whether INSTANCE is empty after locking?

A: to prevent only one object from being instantiated in the case of multithreading concurrency.

For example, thread a and thread b call getInstance method at the same time. If it is judged that INSTANCE is empty at the same time, lock grabbing will be carried out at the same time.
If thread a grabs the lock first and starts executing the code contained in the synchronized keyword, thread b is in a waiting state.
Thread a has created a new INSTANCE and released the lock. At this time, thread b gets the lock and enters the code contained in the synchronized keyword. If it does not judge whether the INSTANCE is empty again, the INSTANCE may be created repeatedly.
Therefore, you need to judge twice before and after synchronized.

Don't think it's over. What's the problem?

2. volatile keyword

What's wrong with the above code?

public static SimpleSingleton4 getInstance() {
      if (INSTANCE == null) {//1
          synchronized (SimpleSingleton4.class) {//2
              if (INSTANCE == null) {//3
                  INSTANCE = new SimpleSingleton4();//4
              }
          }
      }
      return INSTANCE;//5
  }

The code of getInstance method is written in the order of 1, 2, 3, 4 and 5. I hope to execute it in this order.

However, the java virtual machine will actually do some optimization and rearrange some code instructions. After the rearrangement, the order may become: 1, 3, 2, 4 and 5. In this way, multiple instances will be created in the case of multithreading. The rearranged code may be as follows:

public static SimpleSingleton4 getInstance() {
    if (INSTANCE == null) {//1
       if (INSTANCE == null) {//3
           synchronized (SimpleSingleton4.class) {//2
                INSTANCE = new SimpleSingleton4();//4
            }
        }
    }
    return INSTANCE;//5
}

I see. What can be done?

A: the volatile keyword can be added to the definition of INSTANCE. The specific codes are as follows:

public class SimpleSingleton7 {

    private volatile static SimpleSingleton7 INSTANCE;

    private SimpleSingleton7() {
    }

    public static SimpleSingleton7 getInstance() {
        if (INSTANCE == null) {
            synchronized (SimpleSingleton7.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SimpleSingleton7();
                }
            }
        }
        return INSTANCE;
    }
}

volatile keyword can guarantee the visibility of multiple threads, but it cannot guarantee atomicity. It also prevents instruction rearrangement.

The double check lock mechanism not only ensures thread safety, but also improves execution efficiency and saves memory space compared with direct locking.

Besides the above singleton mode, are there any other singleton modes?


4, Static inner class

Static inner classes, as the name suggests, implement the singleton pattern through static inner classes. So, how does it implement singletons?

1. How to implement singleton mode?

How to implement singleton mode?

public class SimpleSingleton5 {

    private SimpleSingleton5() {
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
    }
}

We see that a static Inner class is defined in the SimpleSingleton5 class. In the getInstance method of SimpleSingleton5 class, the INSTANCE instance object of the Inner class is returned.

The virtual machine loads Inner and instantiates the INSTANCE object only when the program calls the getInstance method for the first time.

The internal mechanism of java ensures that only one thread can obtain the object lock, and other threads must wait to ensure the uniqueness of the object.

2. Reflection vulnerability

The above code looks perfect, but there are still loopholes. If others use reflection, they can still create objects through the nonparametric construction of classes. For example:

Class<SimpleSingleton5> simpleSingleton5Class = SimpleSingleton5.class;
try {
    SimpleSingleton5 newInstance = simpleSingleton5Class.newInstance();
    System.out.println(newInstance == SimpleSingleton5.getInstance());
} catch (InstantiationException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
}

The print result of the above code is false.

It can be seen that the object created through reflection is not the same object as the object obtained through getInstance method, that is, this vulnerability will lead to SimpleSingleton5 non singleton.

So, how to prevent this vulnerability?

A: this needs to be judged in the nonparametric construction mode. If it is not empty, an exception will be thrown.

The modified code is as follows:

public class SimpleSingleton5 {

    private SimpleSingleton5() {
        if(Inner.INSTANCE != null) {
           throw new RuntimeException("Duplicate instantiation cannot be supported");
       }
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
        }
    }

}

If at this time, you think this static inner class, the method of implementing singleton mode, is perfect.

Well, what I want to tell you is that you are wrong and there are loopholes...

3. Deserialization vulnerability

As we all know, classes in java can be serialized by implementing the Serializable interface.

We can save the class object to memory or a file first. Later, at a certain time, it will be restored to the original object.

The specific codes are as follows:

public class SimpleSingleton5 implements Serializable {

    private SimpleSingleton5() {
        if (Inner.INSTANCE != null) {
            throw new RuntimeException("Duplicate instantiation cannot be supported");
        }
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
    }

    private static void writeFile() {
        FileOutputStream fos = null;
        ObjectOutputStream oos = null;
        try {
            SimpleSingleton5 simpleSingleton5 = SimpleSingleton5.getInstance();
            fos = new FileOutputStream(new File("test.txt"));
            oos = new ObjectOutputStream(fos);
            oos.writeObject(simpleSingleton5);
            System.out.println(simpleSingleton5.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    private static void readFile() {
        FileInputStream fis = null;
        ObjectInputStream ois = null;
        try {
            fis = new FileInputStream(new File("test.txt"));
            ois = new ObjectInputStream(fis);
            SimpleSingleton5 myObject = (SimpleSingleton5) ois.readObject();

            System.out.println(myObject.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        writeFile();
        readFile();
    }
}

After running, it is found that the hashcodes of serialized and deserialized objects are different:

189568618
793589513

Note: a new object is created during deserialization, which breaks the requirement of singleton mode object uniqueness. So, how to solve this problem?

Answer: rereadresolve method.

In the above example, add the following code:

private Object readResolve() throws ObjectStreamException {
    return Inner.INSTANCE;
}

The operation results are as follows:

290658609
290658609

We see that the hashCode of serialized and deserialized instance objects is the same.

The method is very simple. You only need to return a unique Inner.INSTANCE object every time in the readResolve method. When the program deserializes the object, it will look for the readResolve() method.
If the method does not exist, the new object is returned directly. If the method exists, the object is returned according to the content of the method. If we have not instantiated the singleton before, null will be returned.

Well, come here and finally step on all the pits.
It took a lot of effort.
However, I secretly tell you that there are actually simpler methods, ha ha ha.

what...

5, Enumeration

In fact, enumeration is a natural single instance in java. Each instance has only one object, which is guaranteed by the underlying internal mechanism of java.

Simple usage:

public enum  SimpleSingleton7 {
    INSTANCE;
    
    public void doSamething() {
        System.out.println("doSamething");
    }
} 

Where called:

public class SimpleSingleton7Test {

    public static void main(String[] args) {
        SimpleSingleton7.INSTANCE.doSamething();
    }
}

INSTANCE object INSTANCE is unique in enumeration, so it is a natural singleton pattern.

Of course, in the feature of enumerating object uniqueness, other singleton objects can be created, such as:

public enum  SimpleSingleton7 {
    INSTANCE;
    
    private Student instance;
    
    SimpleSingleton7() {
       instance = new Student();
    }
    
    public Student getInstance() {
       return instance;
    }
}

class Student {
}

The jvm ensures that enumerations are natural singletons, that there are no thread safety issues, and that serialization is supported.

In the classic book Effective Java by Joshua Bloch, the great God of java, it is said:

The enumeration type of single element has become an implementation Singleton The best way.

reference resources

1, official account No. three, thank you very much for an article about technology.


Posted on Wed, 24 Nov 2021 22:44:35 -0500 by JimF