directory

Class loading process 2. Serialization, serialization attack 3. Implementation method 1. 2. lazy 3. static inner class 4. enumeration 4, prevent singleton damage 1. Reflection 2. Deserialize the referenceCopy the code

One, foreword

Singleton patterns are the first design patterns most of us encounter, so there are lots of good and bad sharing online, and even a lot of mistakes. Because it is relatively simple, I did not study and learn by myself, but just absorbed from sharing online. Not long ago, I found some differences with my own cognition in my communication with colleagues, so I reviewed them and got this article.

You can’t learn anything without asking what, why and how:

What is the singleton pattern?

That is, ensure that only one instance of a class is created and that visitors can access the class only through that instance.

Why is the singleton pattern needed?

There are three main points of personal understanding:

  1. Service requirements, such as application configuration classes and main window handles, ensure that there is only one instance globally to avoid service exceptions.
  2. This facilitates unified resource management, such as connection pooling and log output.
  3. Avoid repeated creation and save the consumption of repeated memory opening up instantiation.

How to implement the singleton pattern?

The implementation of singletons is simple and generally includes the following three steps:

  1. Privatized constructs prevent external creation.
  2. Create an instance internally.
  3. Provides static public methods to access instances.

On the concrete implementation, many online sharing provides seven or eight ways, divided into thread safety and unsafe version. Personally, I think it really boils down to just four: hungry, lazy, static inner classes, and enumerations. Because static code blocks and static variables are implemented by hungry people; Both DCL and synchronous methods are slobs. Know their principles, characteristics, attention points, and then adjust according to the needs.

The specific implementation methods will be introduced later, and their characteristics will be summarized here:

The serial number implementation Lazy loading Naturally prevents reflection damage Naturally prevents deserialization from breaking
1 The hungry no no no
2 lazy is no no
3 Static inner class is no no
4 Enumeration class no is is

The reason why the fourth and fifth columns are added natural to prevent singleton destruction is because, as described in Chapter 4, various singleton implementations can prevent reflection/deserialization destruction. Therefore, the word “natural” is added to reflect the prevention of destruction of the original implementation introduced in Chapter 3.

2. Relevant knowledge

1. Class loading process

Let’s review the JVM class loading process, which includes three stages: load, connect, and initialize. Not the focus of this article, so a quick recap:

In the loading phase, the binary byte stream is loaded into the JVM through the class loader and stored in the desired format.

The validation process, primarily for JVM security, checks that the binary byte stream complies with the JVM specification.

The preparation process is mainly to open up memory for static variables and assign default initial values, such as private static Date d = new Date(). In this process, only reference D is opened and assigned to null, instead of executing the constructor. If static, the initialized value is assigned.

The parsing process is basically to replace constant pool symbolic references with direct applications.

The initialization phase, in which the constructor is used to assign the initial value, is for static variables only.

Serialization, serialization attacks

Serialization, serialization attacks, and serialization proxies

Third, implementation method

1. The hungry

The reason why the name is hungry, so the name is “hungry”, when the class is loaded, it is impatient to instantiate the referenced object, therefore, the follow-up do not need to worry about object opening/multi-threading and other problems, but do not have lazy loading characteristics.

The implementation is simple as follows, and initialization can also be performed through static blocks.

public class HungrySingle {
  	// Note point 1: use final modifier
    private static final HungrySingle instance = new HungrySingle();
    private HungrySingle(a) {}
    public static HungrySingle getInstance(a) {
        returninstance; }}Copy the code

It is important to note that references are final in order to prevent instruction reordering from affecting secure publishing. Private static final HungrySingle = new HungrySingle(); It looks like a single line of code, but it’s actually three steps, and it’s not atomic. The steps are as follows:

  1. First, open up memory space for the instance.
  2. Next, the constructor is called to initialize the object and store it in the memory created in Step 1.
  3. Finally, you point the instance reference to the memory space.

Both the virtual machine and the CPU rearrange instructions in different dimensions for efficiency, with getInstance() either occurring before construction is complete (that is, before Step 2) or after construction is complete but step 3 is not. The final modified variable has visibility and prevents instruction reordering from affecting secure release, ensuring that the read and write of the variable occurs after the above three steps are completed.

The following is a reflection and serialization test to verify that a new instance can be created by reflection/serialization to break the singleton:

 public static void main(String[] args) throws Exception {
        LazySingle ori = getInstance();
        // Reflection test
        LazySingle refeInst = LazySingle.class.newInstance();
        System.out.println(refeInst);
        System.out.println(ori);
        System.out.println(refeInst == ori); // The result is false
        // Serialize tests
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(ori);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        LazySingle serializeInst = (LazySingle) objectInputStream.readObject();
        System.out.println(serializeInst);
        System.out.println(ori);
        System.out.println(serializeInst == ori); // The result is false
 }
Copy the code

As you can see, the hungry man implementation does not have the ability to prevent singleton destruction, but we can do additional processing, as described below.

2. Lazy

The reason for the name lazy, compared to hungry, lazy, have procrastination, in the class load only defined an empty reference, does not open up memory for instantiation, until the invocation of the instantiation, so it has lazy loading characteristics.

The implementation is as follows:

public class LazySingle {
    private LazySingle(a) {}
    // Note 1: Use volatile
    private static volatile LazySingle instance = null;
    public static LazySingle getInstance(a) {
        // Double check lock
        if (instance == null) {
            synchronized (LazySingle.class) {
                if (instance == null) {
                    instance = newLazySingle(); }}}returninstance; }}Copy the code

Among them are the following:

As mentioned earlier, instantiation of a reference object is actually a three-step process that is not atomic:

  1. First, open up memory space for the instance.
  2. Next, the constructor is called to initialize the object and store it in the memory created in Step 1.
  3. Finally, you point the instance reference to the memory space.

The virtual machine and CPU rearrange instructions in different dimensions for efficiency, so the execution order of steps 2 and 3 is uncertain: it is possible to point references to memory space before invoking constructors; It is also possible to point a reference to the memory space after the construction is complete.

Therefore, the volatitle variable is used to insert the corresponding memory barrier when the variable is read and written to prevent command reordering from affecting the read and write order. The main difference between volatitle and Fianl is the modification of reference reference.

Adding a null judgment outside of Sync Fast 2 plays a significant role in efficiency, because only when the object is not instantiated, it needs to enter the synchronization block for initialization, and then the synchronization block will affect efficiency more often. Therefore, the null judgment outside can prevent useless synchronization wait and improve efficiency.

The following is a reflection and serialization test to verify that a new instance can be created by reflection/serialization to break the singleton:

    public static void main(String[] args) throws Exception {
        HungrySingle ori = getInstance();
        // Reflection test
        HungrySingle refeInst = HungrySingle.class.newInstance();
        System.out.println(refeInst);
        System.out.println(ori);
        System.out.println(refeInst == ori); // The result is false
        // Serialize tests
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(ori);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        HungrySingle serializeInst = (HungrySingle) objectInputStream.readObject();
        System.out.println(serializeInst);
        System.out.println(ori);
        System.out.println(serializeInst == ori); // The result is false
 }
Copy the code

It can be seen that the lazy implementation also does not have the ability to prevent singleton destruction, which requires us to do additional processing, detailed methods are described below.

Static inner classes

A singleton of a static inner class provides a static inner class inside the class that defines and declares instances in a hanky-hanky-hank fashion. The singleton implementation of static inner classes is lazy because the JVM does not load inner classes immediately, but only when they are used.

The implementation is as follows:

public class InnerSingle {
    private InnerSingle(a) {}
    private static class Inner {
        private static final InnerSingle inst = new InnerSingle();
    }
    public static InnerSingle getInstance(a) {
        returnInner.inst; }}Copy the code

The following is a reflection and serialization test to verify that a new instance can be created by reflection/serialization to break the singleton:

public static void main(String[] args) throws Exception {
        InnerSingle ori = getInstance();
        // Reflection test
        InnerSingle refeInst = InnerSingle.class.newInstance();
        System.out.println(ori);
        System.out.println(refeInst);
        System.out.println(refeInst == ori); // The result is false
        // Serialize tests
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(ori);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        InnerSingle serializeInst = (InnerSingle) objectInputStream.readObject();
        System.out.println(serializeInst);
        System.out.println(ori);
        System.out.println(serializeInst == ori); // The result is false
 }
Copy the code

As you can see, static inner classes also do not prevent singleton destruction and require additional processing, as described below.

4. The enumeration

Since JDK1.5, we have added enumeration types, which are essentially classes that export instances of each enumerated constant through a public static final field. There are no accessible constructors, so when it is loaded into the JVM, it is truly final. Of course, because of this, it does not have the characteristics of lazy loading.

The implementation is as follows. It looks surprisingly simple and not unlike C++, but Java’s enum types are powerful and secure, and you can do a lot with them, which is why Effective Java has always recommended using enumerations first.

public enum EnumSingle {
    INSTANCE;
    EnumSingle() { }
    // Enumerations are actually quite powerful and can do a lot of things here
}
Copy the code

The following is a reflection and serialization test to verify that a new instance can be created by reflection/serialization to break the singleton:

public static void main(String[] args) throws Exception {
        EnumSingle ori = EnumSingle.INSTANCE;
        // Reflection test
        EnumSingle refeInst = EnumSingle.class.newInstance(); // InstantiationException will be thrown
        System.out.println(ori);
        System.out.println(refeInst);
        System.out.println(refeInst == ori);
        // Serialize tests
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(ori);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        EnumSingle serializeInst = (EnumSingle) objectInputStream.readObject();
        System.out.println(serializeInst);
        System.out.println(ori);
        System.out.println(serializeInst == ori); // Result is true
 }
Copy the code

As you can see from the above experiments, enumerations cannot access the constructor through reflection, and the deserialized instance is the same as the original instance, so there is a natural mechanism to prevent singleton destruction through reflection and serialization.

Four, to prevent singleton damage processing method

1. The reflection

As we all know, reflection allows access to private constructors to create new instances. Therefore, to prevent singletons from being destroyed through reflection, we simply check for instances in the constructor and throw an exception if there are, preventing new instances from being created.

// Constructor privateInnerSingle() {
		Assert.check(Inner.inst == null);
}
Copy the code

deserialization

Earlier I wrote about serialization, serialization attacks, and serialization proxies. It is stated that the deserialization mechanism is like an invisible constructor, so new instances can be created through this invisible constructor, thereby breaking the singleton. We can return the deserialized instance with the original instance by providing the readResolve method.

/ /readThe Resolve method returns a public Object with the original instance instead of the deserialized onereadResolve() {
		return Inner.inst;
}
Copy the code

reference

  1. Joshua Bloch, Effective Java