Full Han model
Full Han is the single case model with the most varieties. Starting from the full Han Dynasty, we gradually understand the problems we need to pay attention to when implementing the singleton mode through its variants.
Basic satiated man
A full man is one who is already full and eats when he is not in a hurry. He eats when he is hungry. Therefore, it does not initialize the singleton first, and then initialize it the first time, that is, "lazy loading".
//Full Han // UnThreadSafe public class Singleton1 { private static Singleton1 singleton = null; private Singleton1() { } public static Singleton1 getInstance() { if (singleton == null) { singleton = new Singleton1(); } return singleton; } }
The core of the full man mode is lazy loading. The advantage is faster startup speed and resource saving. You don't need to initialize a single instance until the instance is accessed for the first time; The minor disadvantage is that it is troublesome to write, the major disadvantage is that the thread is unsafe, and there are race conditions in the if statement.
It's not a big problem to write. It's readable. Therefore, in the single thread environment, basic Chinese is the author's favorite writing method. But in a multithreaded environment, the foundation is completely unavailable. The following variants are trying to solve the problem of thread insecurity.
Full Han - variant 1
The crudest violation is to modify the getInstance() method with the synchronized keyword, which can achieve absolute thread safety.
//Full Han // ThreadSafe public class Singleton1_1 { private static Singleton1_1 singleton = null; private Singleton1_1() { } public synchronized static Singleton1_1 getInstance() { if (singleton == null) { singleton = new Singleton1_1(); } return singleton; } }
The advantage of variant 1 is that it is simple to write and absolutely thread safe; The disadvantage is that the concurrency performance is very poor, in fact, it completely degenerates to serial. A singleton only needs to be initialized once, but even after initialization, the synchronized lock cannot be avoided, so getInstance() becomes a serial operation completely. Performance insensitive scenarios are recommended.
Full Han - variant 2
Variant 2 is the "notorious" DCL 1.0.
In view of the problem that the lock cannot be avoided after single instance initialization in variant 1, variant 2 sets another layer of check on the outer layer of variant 1 and the check on the synchronized inner layer, that is, the so-called "Double Check Lock" (DCL for short).
//Full Han // UnThreadSafe public class Singleton1_2 { private static Singleton1_2 singleton = null; public int f1 = 1; //Trigger partial initialization problem public int f2 = 2; private Singleton1_2() { } public static Singleton1_2 getInstance() { // may get half object if (singleton == null) { synchronized (Singleton1_2.class) { if (singleton == null) { singleton = new Singleton1_2(); } } } return singleton; } }
The core of variant 2 is DCL. It seems that variant 2 has achieved the ideal effect: lazy loading + thread safety. Unfortunately, as stated in the comment, DCL is still thread unsafe. Due to instruction reordering, you may get "half an object", that is, the problem of "partial initialization".
How volatile maintains memory visibility: variables modified by volatile keyword see their latest values at any time.
How volatile prevents instruction rearrangement: volatile keyword prevents instructions from being reordered through a "memory barrier".
Problem: if volatile is not added, the instruction replay may cause the {acquisition of half an object:
eg:
1 | instance = new Singleton(); |
It is not an atomic operation. In fact, it can be "abstracted" into the following JVM instructions:
1 2 3 | memory = allocate(); //1: Allocate memory space for objects initInstance(memory); //2: Initialize object (initialize f1 and f2) instance = memory; //3: Set instance to point to the memory address just allocated |
Operation 2 above depends on operation 1, but operation 3 does not depend on operation 2. Therefore, the JVM can reorder them for the purpose of "optimization". After reordering, it is as follows:
1 2 3 | memory = allocate(); //1: Allocate memory space for objects instance = memory; //3: Set instance to point to the memory address just allocated (the object is not initialized at this time) ctorInstance(memory); //2: Initialize object |
You can see that after the instruction rearrangement, operation 3 is ahead of operation 2, that is, when the reference instance points to the memory memory, this new memory has not been initialized - that is, the reference instance points to a "partially initialized object". At this time, if another thread calls the getInstance method, because the instance already points to a piece of memory space, the if condition is judged as false, the method returns the instance reference, and the user gets the "half" single instance that has not completed initialization.
To solve this problem, you only need to declare instance as a volatile variable:
1 | private static volatile Singleton instance; |
Full Han - Variant 3
Variant 3 is specific to variant 2 and can be described as DCL 2.0.
For the "half object" problem of Variant 3, Variant 3 adds the volatile keyword on instance. See the above reference for the principle.
//Full Han // ThreadSafe public class Singleton1_3 { private static volatile Singleton1_3 singleton = null; public int f1 = 1; //Trigger partial initialization problem public int f2 = 2; private Singleton1_3() { } public static Singleton1_3 getInstance() { if (singleton == null) { synchronized (Singleton1_3.class) { // must be a complete instance if (singleton == null) { singleton = new Singleton1_3(); } } } return singleton; } }