Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.
If you think you already know how to implement lazy, IoDH, DCL, enumeration singletons, then go to the last break singletons. If not, I suggest you patiently watch it from the beginning.
By the end of this article you will have mastered the design of all singleton patterns in Java.
The singleton design pattern is one of the most common design patterns in GOF 23. It can be seen in our daily development as well as in some third-party libraries. The singleton pattern is almost always asked about, including in interviews, by junior and middle level programmers.
The purpose of singleton mode is mainly to ensure that the instance is the only solution in multi-threaded scenarios. It is relatively simple to implement, but there are various ways to implement it. Today, the little black belt combs the 7 ways to implement singleton mode, and compares the advantages and disadvantages of each.
The hungry type
Hungry, as the name implies, will create the instance object, because you are very hungry, will create it soon.
/** * @author java_xiaohei public id: "Java * @className Singleton * @description Singleton" Private static Singleton instance = new private static Singleton instance = new private static Singleton instance = new Singleton(); // Constructor is private, New private Singleton() {} public static Singleton getInstance() {return instance; }}Copy the code
The key to hanky-hanky is the direct instantiation when defining instance. It is completely possible to ensure thread-safety of instance objects through handedness.
However, there is a problem. If the instance object is accessed a long time after it is created, the object data will be stored in the heap memory before it is accessed. If the instance data of the singleton object is very large in the actual scenario, it will occupy a lot of resources, which is not appropriate.
LanHanShi
Lazy versus hungry, the main difference is when objects are created. Variables are not initialized when they are defined, but are created when they are needed.
Public final class Singleton {private static Singleton instance = null; // Constructor is private, Public static Singleton getInstance() {if(instance==null){public static Singleton getInstance() {if(instance==null){ instance = new Singleton(); } return instance; }}Copy the code
In this way, compared with hanhanism, you can avoid the waste of space resources caused by creating objects before using them.
The above code has no problem calling getInstance() in single-threaded scenarios, but there are thread safety issues in multi-threaded scenarios.
This is because the if judgment in getInstance() and the assignment to instance are not atomic operations.
Thread A calls getInstance() and thread B calls getInstance(). Thread A determines that instance==null is true.
Before thread A instantiates the object, thread B obtains execution rights from the CPU. If thread B determines that instance==null is true, thread B will also instantiate the object.
This lazy implementation cannot be used in real development because it is not thread-safe.
Lazy + synchronous approach
Simple lazy because there are thread safety issues, then we can lock the way to keep thread safety.
Public final class Singleton {private static Singleton instance = null; // Constructor is private, New private Singleton() {} public static synchronized getInstance() {public static synchronized getInstance() { if(instance==null){ instance = new Singleton(); } return instance; }}Copy the code
By locking the getInstance() method, you ensure that only one thread can enter the method in a multithreaded situation, thus ensuring thread-safety.
However, this approach is a bit lazy. Locking an instance when it is first created prevents multiple instances from being created. However, after the instance is successfully created, the lock must be acquired every time the instance is acquired, which can greatly reduce performance.
DCL
DCL stands for Double Check Lock.
public final class Singleton { private static Singleton instance = null; String msg; Private Singleton() {// initialize MSG this. MSG = "object description "; } public static Singleton getInstance() {if (instance == null) {// Only one thread can acquire Singleton class lock synchronized (Singleton. Class) {if (instance == null) {instance = new Singleton(); } } } return instance; }}Copy the code
The DCL approach does not slacken the thread-safe approach and does not directly lock methods.
The first level determines that if true, locking and instance creation will be performed. If the value of the first layer is false, it indicates that the object has been created. Therefore, it is OK to return the instance directly, avoiding locking every request and achieving high performance.
If the result is true at the first level, locking is required to ensure the security of the instance creation process.
The purpose of the second layer empty check after the lock is successful is to prevent the instance from being created by other threads before the first layer check and the lock is successful, and avoid repeated creation.
The DCL approach is thread-safe without compromising performance and is lazy to load, which seems to be the perfect solution.
However, this approach can cause null-pointer exceptions in multithreaded situations. If you take a closer look at my code, you’ll notice that in the above code, the Singleton() constructor does an initial assignment to the property MSG.
So instance = new Singleton() in getInstance(); It can be divided into three steps.
1. New Singleton() creates an object in the heap;
2. MSG assignment;
3. The instance assignment;
However, according to the JVM run-time happens-before rule, the order of these three steps does not depend on each other, and it is very likely that the actual order will be 3->1->2 or something else; Instance == null in getInstance() is true, but MSG in the instance is null; If the caller takes the instance and uses MSG directly, it will throw a null pointer exception.
volatile+DCL
The DCL approach seems to be a neat implementation of the singleton pattern, but the JVM’s runtime instruction reordering causes exceptions in singleton usage.
To solve this problem, use the volatile keyword.
Public final class Singleton {private static volatile Singleton instance = null; String msg; Private Singleton() {this.msg = ""; } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }}Copy the code
The volatile keyword prevents reordering. If you’re interested in the underlying principles of volatile, take a look at my other article. Java memory model
Static inner class
The way to implement the singleton pattern using static inner classes is largely through the classloading mechanism.
Public final class Singleton {private Singleton() {} public static Singleton getInstance() {// Holder.instance; } private static Singleton instance = new Singleton(); private static Singleton instance = new Singleton(); }}Copy the code
This puts the definition of a singleton in the static inner class Holder, which is pretty obvious if you’re familiar with static inner class loading.
Static inner classes are not loaded along with external classes, but only when used;
The process of class loading directly ensures thread-safety and the uniqueness of instance objects.
The IoDH(Initialization Demand Holder) technique is one of the most widely used methods and is the best singleton design pattern.
The enumeration
A final way to implement singletons is through enumerations.
public enum Singleton { INSTANCE; String data = "instance data "; Singleton() { } public static Singleton getInstance() { return INSTANCE; }}Copy the code
Using enumerations to implement singletons is recommended in Effective Java **. Enumerations cannot be inherited and are thread-safe and are instantiated only once.
Break the singleton
With all this talk about singletons, is it possible to ensure that instances are created only once? Can it be broken through?
We learned about a technique called reflexes, and we can try to see if we can make things work, using the hungry style for example.
public final class Singleton { private static Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; } } class SingletonTest { public static void main(String[] args) throws Exception { Singleton instance = Singleton.getInstance(); System.out.println(instance); Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton singleton = constructor.newInstance(); System.out.println(singleton); }}Copy the code
Running results:
As a result, simply privatizing the constructor does not guarantee instance creation, and this limitation can be overcome by using the square reflection processing constructor.
To prevent the user from doing this, we can tweak the constructor.
public final class Singleton { private static Singleton instance = new Singleton(); private Singleton() { if(Singleton.instance ! = null){throw new IllegalStateException(" you don't mess up "); } } public static Singleton getInstance() { return instance; }}Copy the code
If instance is already instantiated, an exception is thrown to prevent reflection from breaking the singleton. Being lazy is not guaranteed to be created before the getInstance() method is called.
As we will see here, whether lazy or hungry, privatizing a constructor does not guarantee that the constructor will not be executed externally, so can enumerations be broken out of uniqueness? Let’s try it again.
public enum Singleton { INSTANCE; String data = "instance data "; Singleton() { } public static Singleton getInstance() { return INSTANCE; } } class SingletonTest { public static void main(String[] args) throws Exception { Singleton instance = Singleton.getInstance(); System.out.println(instance); Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton singleton = constructor.newInstance(); System.out.println(singleton); }}Copy the code
Running results:
It turns out that the enumeration type has a private constructor from the code, but is not available through reflection execution. Does enumeration have no constructor?
Enumeration class does not have a constructor with no arguments, but does have a method with arguments. The first parameter is String and the second parameter is int. I guess these two parameters should be the name and ordinal properties of the enumeration object. Let’s do another thing.
class SingletonTest { public static void main(String[] args) throws Exception { Singleton instance = Singleton.getInstance(); System.out.println(instance); Constructor<? >[] declaredConstructors = Singleton.class.getDeclaredConstructors(); Constructor<? > constructor = declaredConstructors[0]; constructor.setAccessible(true); // Create an Object singleton with the specified parameter constructor. NewInstance ("INSTANCE", 1); System.out.println(singleton); }}Copy the code
Execution Result:
We found that an exception was thrown when we got the constructor by reflection and then created the object with the parameters we saw with Debug.
Cannot reflectively create enum objects. Cannot reflect to create enumeration object!!
It turns out that the JDK has long anticipated the possibility of someone doing something like this to break the uniqueness of enumerated objects, and this is part of the reason why Effective Java recommends it.
conclusion
Singleton pattern in the interview or in the actual development process will appear very high frequency, the looks be like simple, but to achieve a thread safe, high performance, and can not be illegal external damage, need to consider the point very much, the little black usually achieved by a static inner class or enumeration method to design the singleton IoDH way.
If you think it helps, give it a “like” before you leave.