preface

The singleton pattern should be considered the most common and easiest knowledge point among the 23 design patterns. Interviewers will often ask you to write singletons. Don’t be silly and say I don’t know.

Earlier, I described several common ways to write a singleton pattern. If you don’t know yet, the portal is here:

Singletons of design patterns

This article will expand on some of the less obvious questions. Take a look at the problems with the traditional singleton pattern and suggest solutions. Let the interviewer’s eyes shine, heart said, the boy has something!

Here, take the DCL singleton pattern as an example.

DCL singleton pattern

DCL stands for Double Check Lock. The code is as follows,

public class Singleton {

    // Note that this variable needs to be volatile to prevent instruction reordering
    private static volatile Singleton singleton = null;

    private Singleton(a){}public static Singleton getInstance(a){
        // Check if the instance is empty to see if you need to enter the synchronized code block
        if(singleton == null) {synchronized (Singleton.class){
                // Check again if the instance is empty when entering the synchronized code block
                if(singleton == null){
                    singleton = newSingleton(); }}}returnsingleton; }}Copy the code

At first glance, this looks fine, and we often do.

But here’s the problem.

Do DCL singletons ensure thread-safety?

Some people will say, “This is not nonsense, everyone does not write it, it must be thread safe ah.

Indeed, under normal circumstances, I can guarantee that I call the getInstance method twice and get the same object.

However, we do know that There is one powerful feature in Java — reflection. That’s right. That’s right. That’s him.

With reflection, I can break the singleton pattern and call its constructor to create a different object.

public class TestDCL {
    public static void main(String[] args) throws Exception {
        Singleton singleton1 = Singleton.getInstance();
        System.out.println(singleton1.hashCode()); / / 723074861
        Class<Singleton> clazz = Singleton.class;
        Constructor<Singleton> ctr = clazz.getDeclaredConstructor();
        // Get the no-parameter construct by reflection and make it accessible
        ctr.setAccessible(true);
        Singleton singleton2 = ctr.newInstance();
        System.out.println(singleton2.hashCode()); / / 895328852}}Copy the code

We’ll see that we can create objects by calling the no-argument constructor directly through reflection. I don’t care if your constructor is private, there is no privacy under reflection.

The printed hashCode is different, indicating that these are two different objects.

So how do we prevent reflection from destroying singletons?

Very simple, since you want to create an object with a no-argument construct, I’ll make one more judgment in the constructor. If the singleton is already created, I just throw an exception and don’t let you create it.

Modify the constructor as follows,

Run the test code again, and an exception will be thrown.

Effectively preventing the creation of objects through reflection.

So, is this ok for singletons?

At this point, the clever friend will certainly say, since asked, that is a question (really a clever boy).

But what’s the problem?

As we know, objects can also be serialized and deserialized. So what if I serialize the singleton, and deserialize the next one, or the previous singleton?

The proof is in the pudding. Let’s test it out.

// Add the serialization flag to the Singleton to indicate that it can be serialized
public class Singleton implements Serializable{...// omit unimportant code
}
// Tests whether the same object is returned
public class TestDCL {
    public static void main(String[] args) throws Exception {
        Singleton singleton1 = Singleton.getInstance();
        System.out.println(singleton1.hashCode()); / / 723074861
        // Serialize the object and deserialize the new object
        String filePath = "D:\\singleton.txt";
        saveToFile(singleton1,filePath);
        Singleton singleton2 = getFromFile(filePath);
        System.out.println(singleton2.hashCode()); / / 1259475182
    }

    // Write the object to a file
    private static void saveToFile(Singleton singleton, String fileName){
        try {
            FileOutputStream fos = new FileOutputStream(fileName);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(singleton); // Write the object to oos
            oos.close();
        } catch(IOException e) { e.printStackTrace(); }}// Read objects from files
    private static Singleton getFromFile(String fileName){
        try {
            FileInputStream fis = new FileInputStream(fileName);
            ObjectInputStream ois = new ObjectInputStream(fis);
            return (Singleton) ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null; }}Copy the code

You can see that when I serialize the singleton, and then deserialize it, it’s not the same object anymore. Therefore, singletons are broken.

So how do you solve this problem?

I’ll start with the solution and explain why this works.

Simply add a method readResolve to the singleton class, and tell it to return the singleton object we created.

Then run the test class again and see that the printed hashCode is the same.

Isn’t it amazing…

Why does readResolve solve serial-breaking singletons?

We can solve the doubts in our hearts by looking at some key steps in the source code.

Let’s think about which of the serialization and deserialization processes is most likely to have room for manipulation.

First, when serializing, you convert an object to binary and store it in an ObjectOutputStream. There seems to be nothing special about this place.

Second, it’s all about deserialization. When deserializing, you need to read an object from an ObjectInputStream object. The normal read object is a new and different object. Why should you read the same object this time?

It should be possible. So, go to the method we wrote getFromFile and find this line ois.readObject(). It is the method of reading objects from the stream.

Point in, view the ObjectInputStream readObject methods, and then find readObject0 () method

Click on the TC_OBJECT branch again and we find a switch judgment. It is used to deal with object types.

Then you see that there is a readOrdinaryObject method, click on it.

Then you go to this line, the isInstantiable() method, which tells you if the object isInstantiable.

Since the cons constructor is not empty, this method returns true. Therefore, a non-empty OBj object is constructed.

Further down the line, call the hasReadResolveMethod method to determine whether the readResolveMethod variable is non-empty.

So let’s look at this variable and see if there’s an assignment there. You’ll find a code that looks like this,

Click on this method getInheritableMethod. It was finally discovered to return the readResolve method we added.

We also discovered that the method’s modifier can be public, protected, or private (we’re currently using private). However, the static and abstract modifications are not allowed.

Go back to the readOrdinaryObject method again and further down, you’ll see that the invokeReadResolve method is called. This method, which calls the readResolve method through reflection, yields the REP object.

Then, determine whether rep is equal to obj. Obj is the new object we just created through the constructor, and since we overrode the readResolve method to return a singleton directly, rep is the original singleton and is not equal to obj.

So, rep is assigned to obj, and obj is returned.

So, you end up with this obj object, which is our original singleton.

So here we see what’s going on.

In a nutshell: When an object is read from ObjectInputStream, it checks that the object’s class negates the readResolve method. If so, we call it to return the object we want to specify (in this case, return singleton).

conclusion

So the complete DCL can be written like this,

public class Singleton implements Serializable {

    // Note that this variable needs to be volatile to prevent instruction reordering
    private static volatile Singleton singleton = null;

    private Singleton(a){
        if(singleton ! =null) {throw new RuntimeException("Can not do this"); }}public static Singleton getInstance(a){
        // Check if the instance is empty to see if you need to enter the synchronized code block
        if(singleton == null) {synchronized (Singleton.class){
                // Check again if the instance is empty when entering the synchronized code block
                if(singleton == null){
                    singleton = newSingleton(); }}}return singleton;
    }

    // Define the readResolve method to prevent deserialization from returning different objects
    private Object readResolve(a){
        returnsingleton; }}Copy the code

In addition, I do not know if careful readers have noticed that the switch branch has a case TC_ENUM branch in the source code. In this case, the processing is done for enumeration types.

If you’re interested, you can read up on it. The net effect is that we define singletons by enumeration, which prevents serialization from destroying singletons.

Wechat search “misty rain starry sky”, white piao more good articles ~