- 1. What is the single case?
- 1.1 advantages
- 1.2 disadvantages
- 2. Several methods of destroying single case
- 2.1 creating objects through constructors
- 2.2 reflection call constructor
- 2.3 the clonable interface is implemented
- 2.4 single example of serialization failure
- 3. Summary
1. What is the single case?
Singleton pattern: it is a kind of creation design pattern, which aims to ensure that there is only one instance object in a global class. It is divided into lazy and hungry. The so-called lazy type is similar to lazy loading, which triggers the initialization of instance objects when necessary. On the contrary, when the project is started and the class is loaded, the initialization singleton object will be created.
1.1 advantages
If there is only one instance, it can occupy less system resources, save memory and access relatively quickly. Quite flexible.
1.2 disadvantages
It cannot be used on changing objects, especially objects with different attributes caused by different requests. Since the default instance of Spring itself is a singleton, it is necessary to judge the application scenario when using it, so as not to make a fuss. It is often difficult to find this strange problem by manipulating references and collections. For example, for some configuration acquisition, if the value needs to be modified for later use, either define the use of singleton, use deep copy later, or do not use singleton.
Since the singleton mode is used, you have to do everything possible to ensure that the instance is unique, which is also the mission of the singleton mode. But the code is written by people. No matter how perfect people may write less perfect code, no matter how safe the system is, there may be loopholes. Since you want to guarantee singleton, how can I find a way to create multiple different objects of the same class? This is the destruction of the singleton mode. What are the ways to destroy the singleton mode? Mainly but not limited to the following:
- The constructor is not privatized and can be called directly.
- Reflection call constructor
- The clonable interface is implemented
- Serializable and Deserialize
2. Several methods of destroying single case
2.1 creating objects through constructors
Generally speaking, a little ✔️ The singleton mode of cannot create objects through new. This strictly does not belong to the destruction of singleton mode. However, people are not perfect, and the written program cannot be perfect. Sometimes they neglect and forget to privatize the constructor, so the external can call the constructor directly, and the singleton mode can be destroyed naturally. Therefore, this writing method is an unsuccessful singleton mode.
/** * The following is a single example of using double check lock */ public class Singleton{ private volatile static Singleton singleton; public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
The above is to use the double check lock to realize the singleton mode, but forget to write the private constructor. By default, there is a public constructor. What will happen if it is called?
public static void main(String[] args) { Singleton singleton = new Singleton(); Singleton singleton1 = new Singleton(); System.out.println(singleton.hashCode()); System.out.println(singleton1.hashCode()); System.out.println(Singleton.getSingleton().hashCode()); }
The results of the operation are as follows:
692404036 1554874502 1846274136
The hashcode s of the three objects are different, so they are not the same object. This proves that this singleton writing method is unsuccessful.
2.2 reflection call constructor
If the singleton class has declared the constructor to be private, it is temporarily impossible to explicitly call the constructor, but is there really no other method to destroy the singleton?
The answer is yes! That is, modify the permission by calling the construction method through reflection.
For example, a seemingly perfect singleton mode:
import java.io.Serializable; public class Singleton{ private volatile static Singleton singleton; private Singleton(){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
The test code is as follows:
import java.lang.reflect.Constructor; public class SingletonTests { public static void main(String[] args) throws Exception { Singleton singleton = Singleton.getSingleton(); Singleton singleton1=Singleton.getSingleton(); Constructor constructor=Singleton.class.getDeclaredConstructor(null); constructor.setAccessible(true); Singleton singleton2 =(Singleton) constructor.newInstance(null); System.out.println(singleton.hashCode()); System.out.println(singleton1.hashCode()); System.out.println(singleton2.hashCode()); } }
Operation results:
692404036 692404036 1554874502
From the results, we can see that radiation can indeed call the privatized constructor and construct different objects, thus breaking the singleton pattern.
Is there any way to prevent damage in this situation? Since you want to prevent damage, you must prevent calling the private constructor, that is, after calling once, you will report an error and throw an exception. Our singleton mode can be written as follows:
import java.io.Serializable; public class Singleton { private static int num = 0; private volatile static Singleton singleton; private Singleton() { synchronized (Singleton.class) { if (num == 0) { num++; } else { throw new RuntimeException("Don't use this method"); } } } public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
The test call method remains unchanged, and the test results are as follows. An exception is thrown during reflection call, which shows that it can effectively prevent reflection call from destroying the singleton mode:
Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at singleton.SingletonTests.main(SingletonTests.java:11) Caused by: java.lang.RuntimeException: Don't use this method at singleton.Singleton.<init>(Singleton.java:15) ... 5 more
2.3 the clonable interface is implemented
If the singleton object has declared the constructor private and overridden the constructor, the constructor cannot be called for the time being. However, there is another case, that is, copying, which does not need to go through the construction method. However, to copy, you must implement the Clonable method and override the clone method.
import java.io.Serializable; public class Singleton implements Cloneable { private static int num = 0; private volatile static Singleton singleton; private Singleton() { synchronized (Singleton.class) { if (num == 0) { num++; } else { throw new RuntimeException("Don't use this method"); } } } public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
The test code is as follows:
public class SingletonTests { public static void main(String[] args) throws Exception { Singleton singleton1=Singleton.getSingleton(); System.out.println(singleton1.hashCode()); Singleton singleton2 = (Singleton) singleton1.clone(); System.out.println(singleton2.hashCode()); } }
The running results are as follows. The hashcodes of the two objects are inconsistent, which proves that if you inherit the clonable interface and rewrite the clone() method, the singleton of this class can be broken and different objects can be created. However, this clone destroys the singleton mode, which looks more like actively destroying the singleton mode. What do you mean?
That is, if we only want a single instance many times, but in rare cases, we want multiple objects, we can use this method, which is more like leaving a back door for ourselves. It can be considered a benign way to destroy a single instance.
2.4 single example of serialization failure
Serialization is actually similar to clone, but the difference is that many of our objects must implement the serialization interface, but what are the risks of ensuring singleton after implementing the serialization interface?
The risk is that after serialization and then deserialization, the content of the object is the same, but the object is not the same object. Don't believe it? Then try:
The single example is defined as follows:
import java.io.Serializable; public class Singleton implements Serializable { private static int num = 0; private volatile static Singleton singleton; private Singleton() { synchronized (Singleton.class) { if (num == 0) { num++; } else { throw new RuntimeException("Don't use this method"); } } } public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
The test code is as follows:
import java.io.*; import java.lang.reflect.Constructor; public class SingletonTests { public static void main(String[] args) throws Exception { Singleton singleton1 = Singleton.getSingleton(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("file")); objectOutputStream.writeObject(singleton1); File file = new File("tempFile"); ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file)); Singleton singleton2 = (Singleton) objectInputStream.readObject(); System.out.println(singleton1.hashCode()); System.out.println(singleton2.hashCode()); } }
The above code first serializes the object to the file, and then deserializes it back from the file. The results are as follows:
2055281021 1198108795
The results show that the hashcodes of the two objects are different, indicating that the single instance of this class has been destroyed.
So is there a way to prevent single case damage in this case? The answer is: Yes!!!.
Since objectinputstream. Is called Read object () to deserialize. Let's look at the source code, which calls the readObject() method.
public final Object readObject() throws IOException, ClassNotFoundException { return readObject(Object.class); }
readObject() method, which calls readObject0() method:
private final Object readObject(Class<?> type) throws IOException, ClassNotFoundException { if (enableOverride) { return readObjectOverride(); } if (! (type == Object.class || type == String.class)) throw new AssertionError("internal error"); // if nested read, passHandle contains handle of enclosing object int outerHandle = passHandle; try { // serialized objects Object obj = readObject0(type, false); handles.markDependency(outerHandle, passHandle); ClassNotFoundException ex = handles.lookupException(passHandle); if (ex != null) { throw ex; } if (depth == 0) { vlist.doCallbacks(); } return obj; } finally { passHandle = outerHandle; if (closed && depth == 0) { clear(); } } }
readObject0() is internally as follows. In fact, it is processed separately for different types:
private Object readObject0(Class<?> type, boolean unshared) throws IOException { boolean oldMode = bin.getBlockDataMode(); if (oldMode) { int remain = bin.currentBlockRemaining(); if (remain > 0) { throw new OptionalDataException(remain); } else if (defaultDataEnd) { /* * Fix for 4360508: stream is currently at the end of a field * value block written via default serialization; since there * is no terminating TC_ENDBLOCKDATA tag, simulate * end-of-custom-data behavior explicitly. */ throw new OptionalDataException(true); } bin.setBlockDataMode(false); } byte tc; while ((tc = bin.peekByte()) == TC_RESET) { bin.readByte(); handleReset(); } depth++; totalObjectRefs++; try { switch (tc) { // null case TC_NULL: return readNull(); // reference type case TC_REFERENCE: // check the type of the existing object return type.cast(readHandle(unshared)); // class case TC_CLASS: if (type == String.class) { throw new ClassCastException("Cannot cast a class to java.lang.String"); } return readClass(unshared); // agent case TC_CLASSDESC: case TC_PROXYCLASSDESC: if (type == String.class) { throw new ClassCastException("Cannot cast a class to java.lang.String"); } return readClassDesc(unshared); case TC_STRING: case TC_LONGSTRING: return checkResolve(readString(unshared)); // array case TC_ARRAY: if (type == String.class) { throw new ClassCastException("Cannot cast an array to java.lang.String"); } return checkResolve(readArray(unshared)); // enumeration case TC_ENUM: if (type == String.class) { throw new ClassCastException("Cannot cast an enum to java.lang.String"); } return checkResolve(readEnum(unshared)); // object case TC_OBJECT: if (type == String.class) { throw new ClassCastException("Cannot cast an object to java.lang.String"); } return checkResolve(readOrdinaryObject(unshared)); // abnormal case TC_EXCEPTION: if (type == String.class) { throw new ClassCastException("Cannot cast an exception to java.lang.String"); } IOException ex = readFatalException(); throw new WriteAbortedException("writing aborted", ex); case TC_BLOCKDATA: case TC_BLOCKDATALONG: if (oldMode) { bin.setBlockDataMode(true); bin.peek(); // force header read throw new OptionalDataException( bin.currentBlockRemaining()); } else { throw new StreamCorruptedException( "unexpected block data"); } case TC_ENDBLOCKDATA: if (oldMode) { throw new OptionalDataException(true); } else { throw new StreamCorruptedException( "unexpected end of block data"); } default: throw new StreamCorruptedException( String.format("invalid type code: %02X", tc)); } } finally { depth--; bin.setBlockDataMode(oldMode); } }
You can see that the readOrdinaryObject() method is called when processing the object. Here comes the good guy:
private Object readOrdinaryObject(boolean unshared) throws IOException { if (bin.readByte() != TC_OBJECT) { throw new InternalError(); } ObjectStreamClass desc = readClassDesc(false); desc.checkDeserialize(); Class<?> cl = desc.forClass(); if (cl == String.class || cl == Class.class || cl == ObjectStreamClass.class) { throw new InvalidClassException("invalid class descriptor"); } Object obj; try { // reflex obj = desc.isInstantiable() ? desc.newInstance() : null; } catch (Exception ex) { throw (IOException) new InvalidClassException( desc.forClass().getName(), "unable to create instance").initCause(ex); } passHandle = handles.assign(unshared ? unsharedMarker : obj); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null) { handles.markException(passHandle, resolveEx); } if (desc.isExternalizable()) { readExternalData((Externalizable) obj, desc); } else { readSerialData(obj, desc); } handles.finish(passHandle); // If the hasReadResolveMethod () method is implemented if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { // Execute the hasReadResolveMethod() method 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; }
From the diamante above, we can see that the bottom layer realizes serialization through reflection. What if we don't want it to reflect? Then you can see that after reflection, there is actually a search related to the readResolveMethod() method. If readResolveMethod() is implemented, call the method directly to return the result instead of the result after reflection call. This reflects, but it doesn't work.
If we override the readResolveMethod() method, we can return our object directly instead of the object after reflection.
try?
We transform the singleton mode into this:
import java.io.Serializable; public class Singleton implements Serializable,Cloneable { private static int num = 0; private volatile static Singleton singleton; private Singleton() { synchronized (Singleton.class) { if (num == 0) { num++; } else { throw new RuntimeException("Don't use this method"); } } } public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } // Prevent anti sequence reflection from generating objects private Object readResolve() { return singleton; } }
The test code remains unchanged, and the results are as follows. Facts have proved that this is true. Deserialization will not re reflect the object. It has always been the same object, and the problem has been solved perfectly.
2055281021 2055281021
3. Summary
A slightly perfect singleton will not let others call the constructor, but the private constructor can not completely prevent the destruction of the singleton. If reflection is used, it can still be illegally called to the constructor, because we need a number of times. If the constructor calls too many times, it will directly report an error.
But sometimes we want to leave a small back door, so we can't break the singleton mode most of the time. By implementing clonable and rewriting the clone() method, you can generate different objects.
Serialization is a bit like clone(). Both of them actively provide destruction methods, but they often have to provide serialization interfaces, but they don't want to be destroyed. At this time, you can rewrite the readResolve() method to directly return objects without returning objects generated by reflection, so as to protect the singleton mode from destruction.