preface
Singleton pattern, should be a high frequency of use of a design pattern.
Do you know enough about it? Such as:
How are Java and Kotlin implemented? What exactly does lazy and hungry mean? The implementation principle of hanhan-hungry, double check, static inner class pattern? Class initialization, class locking, thread safety, Kotlin syntax involved?
Static variable implementation singleton – hungry man
Ensuring an instance is easy, as long as the same instance is returned each time, the key is how to ensure that the instantiation process is thread safe?
Let’s review class initialization.
Before the class is instantiated, the JVM performs class loading.
The final step in class loading is class initialization. In this phase, the class constructor
method is executed. Its main job is to initialize the static variables, code blocks in the class.
The
() method blocks. In a multi-threaded environment, if multiple threads initialize a class at the same time, only one thread will execute the
() of the class, and all other threads will block. In other words, the < Clinit > method is endowed with thread-safe capabilities.
Combined with the singleton we are implementing, it is easy to imagine that we can create this singleton as a static variable. This process is thread-safe, so we arrive at the first singleton implementation method:
private static Singleton singleton = new Singleton();
public static Singleton getSingleton(a) {
return singleton;
}
Copy the code
It is simple to implement a unique singleton through static variables, and thread-safe.
The downside of what might seem like a perfect method is that it is possible to load the class without calling the getSingleton method, for example using reflection or some other static variable static method in the class. So the downside of this approach is that it can be a waste of resources, and I instantiate the singleton when I don’t use it.
Under the same classloader, a type is initialized only once, and there are six times when class initialization can be triggered:
- 1. Initialize the main class containing the main method when the VM starts.
- 2. When directives such as new create object instances
- 3. When accessing instructions for static methods or fields
- 4, the initialization process of a subclass if it is found that its parent class has not been initialized
- 5. When making reflection calls using the reflection API
- 6, the first call to Java. Lang. Invoke the MethodHandle instance
I don’t care if you use it or not, as soon as my class is initialized, I’m instantiating this singleton, which is analogous to a hungry man method. (Is really hungry, first instantiate out put it, you can eat directly when you want to eat)
The disadvantage is that it may cause a waste of resources (in the end, the meal is not eaten, the meal is wasted)
However, this pattern is usually sufficient, because the class is usually used when the instance is used, and it is rare that you need to use the class without using its singleton.
Of course, there are better ways to deal with this potential waste of resources.
Before we do that, let’s look at Kotlin’s hungry man implementation.
Kotlin hungry-the simplest singleton
object Singleton
Copy the code
Didn’t? Well, no.
There’s a kotlin keyword involved: object.
There are three main uses of object:
- Object expression
Primarily used to create an object that inherits from an anonymous class of some type (or some type).
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { / *... * / }
override fun mouseEntered(e: MouseEvent) { / *... * /}})Copy the code
- Object statement
Mainly used for singletons. That’s how we use it today.
object Singleton
Copy the code
We can see the decomcompiled Java code using Android Studio’s Show Kotlin Bytecode function:
public final class Singleton {
public static final Singleton INSTANCE;
private Singleton(a) {}static {
Singleton var0 = newSingleton(); INSTANCE = var0; }}Copy the code
Obviously, much like we wrote in the last section, singletons are instantiated during the initialization phase of a class, but one through a static code block and one through a static variable.
- Associated object
Object declarations within a class can be marked with the Companion keyword, and are somewhat like static variables, but not really static variables.
class MyClass {
companion object Factory {
fun create(a): MyClass = MyClass()
}
}
/ / use
MyClass.create()
Copy the code
Decompiling into Java code:
public final class MyClass {
public static final MyClass.Factory Factory = new MyClass.Factory((DefaultConstructorMarker)null);
public static final class Factory {
@NotNull
public final MyClass create(a) {
return new MyClass();
}
private Factory(a) {}// $FF: synthetic method
public Factory(DefaultConstructorMarker $constructor_marker) {
this(a); }}}Copy the code
The principle is still a static inner class, and the method of the static inner class is called, but the name of the static inner class is omitted.
To implement a truly static member, you need to decorate the @jVMField variable.
Optimize hungry people, eat at the time to cook – the most elegant singleton
To get back to the point, even if hungry people have shortcomings, we should try to solve them. What way can we not waste this example? So load on demand singletons, right?
This brings us to another point, which is the loading timing of static inner classes.
When we talk about loading classes, the initialization process only loads static variables and code blocks, so we don’t load static inner classes.
Static inner classes are delayed-loaded, meaning they are loaded only when the inner class is explicitly used, not when the outer class is used only.
Based on this information, we can optimize the hunchman schema to static inner class schema (Java and Kotlin versions) :
private static class SingletonHolder {
private static Singleton INSTANCE = new Singleton();
}
public static Singleton getSingleton(a) {
return SingletonHolder.INSTANCE;
}
Copy the code
companion object {
val instance = SingletonHolder.holder
}
private object SingletonHolder {
val holder = SingletonDemo()
}
Copy the code
Again, thread safety is ensured through the class initialization
() method, and on top of that, singleton instantiation is moved back to static inner classes. So the static inner class is initialized when the getSingleton method is called, and the static singleton is instantiated.
This whole, this method is perfect… ? For example, when I call getSingleton to create an instance, I want to pass in parameters.
You can, but you need to set the parameter values first. You cannot set the parameters dynamically by calling the getSingleton method. For example:
private static class SingletonHolder {
private static String test="123";
private static Singleton INSTANCE = new Singleton(test);
}
public static Singleton getSingleton(a) {
SingletonHolder.test="12345";
return SingletonHolder.INSTANCE;
}
Copy the code
The final instantiated test will only be 123, not 12345. Because as soon as you start using the SingletonHolder inner class, the singleton INSTANCE is instantiated initially, even if you assign test, that’s after the singleton INSTANCE is instantiated.
This is the disadvantage of static inner class methods. If you do not pass parameters dynamically, this method is sufficient.
Can pass the singleton – lazy
What if you need to pass parameters?
Call getSingleton to check if the singleton already exists and instantiate it if it doesn’t.
private static Singleton singleton;
public static Singleton getSingleton(a) {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
Copy the code
This is a very clear view of the need to create instances, so that you can ensure that when you need to eat to cook, a more formal approach. But in the mind of a hungry person, they would think that the person is very lazy. They don’t prepare food first and then cook it when they eat.
Therefore, this method is called the slacker style.
However, the downside of this method is that it is not thread safe. Multiple threads accessing the getSingleton method at the same time can cause object instantiation errors.
So, lock.
Double-checked slob
Add lock how add, also be a problem.
First of all, the lock must be a class lock, because we need to lock the class so that only one thread can instantiate the singleton at a time.
Class locks can then be added in two ways, modifying static methods and modifying class objects:
Method 1, modify static methods
public synchronized static Singleton getSingleton(a) {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
// Method 2, code block decorates the class object
public static Singleton getSingleton(a) {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = newSingleton(); }}}return singleton;
}
Copy the code
Method 2 is what we call a double check pattern.
In fact, the difference between the two methods is that in this double check, the singleton is first judged to be empty. If it is empty, the singleton’s instantiation code is normally used in the lock stage.
So why do it?
The first judgment is for performance
. When the singleton has been instantiated, we actually do not need to enter the locking stage again. Therefore, the first judgment is to reduce locking. Locking is controlled only during the first instantiation, and then the singleton can be obtained directly.The second judgment is to prevent duplicate object creation
. When two threads go tosynchronized
Here, thread A acquires the lock and goes to create the object. After the object is created, the lock is released, and thread B acquires the lock. If the singleton is not null, the object is created again and the operation is repeated.
At this point, it looks like the problem is solved.. ?
Wait, is the instantiation of new Singleton() actually okay?
In the JVM, there is an operation called instruction rearrangement:
In order to optimize instructions and improve program efficiency, the JVM reorders instructions without affecting the execution results of single-threaded programs, but this reordering does not affect single-threaded programs.
Simply put, some instructions may be scrambled without affecting the final result.
If you look at the instructions in object instantiation, there are three main steps:
- 1. Allocate object memory space
- 2. Initialize the object
- 3, Instance refers to the newly allocated memory address
If we rearrange the second and third steps, the result will not matter:
- 1. Allocate object memory space
- 2. Instance refers to the newly allocated memory address
- 3. Initialize the object
In this case, there is a problem:
When thread A enters the instantiation phase, that is, the new Singleton(), it has just completed the second step of allocating the memory address. Thread B calls the getSingleton() method, goes to the first null, finds that it is not null, and returns a singleton.
This is where reordering can lead to problems.
So we need to disallow the reordering of instructions, and make the entry of volatile.
Volatile has two main characteristics:
- Visibility. That is, write operations are visible to other threads.
- Disallow command reordering.
So the addition of volatile modifiers completes the double-checked singleton pattern.
private volatile static Singleton singleton;
Copy the code
Kotlin version double check
// No parameters
class Singleton private constructor() {
companion object {
val instance: Singleton by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
Singleton() }
}
}
/ / parameters
class Singleton private constructor(private val context: Context) {
companion object {
@Volatile private var instance: Singleton? = null
fun getInstance(context: Context)= instance ? : synchronized(this) { instance ? : Singleton(context).apply { instance =this}}}}Copy the code
Writing with parameters is easy to understand, similar to Java. But isn’t it a little easy to write this without arguments? And Volatile is gone? Sure?
Lazy is no problem, in this delay to attribute (mode = LazyThreadSafetyMode. SYNCHRONIZED), we look into:
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
private class SynchronizedLazyImpl<out T> (initializer: () - >T.lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
privateval lock = lock ? :this
override val value: T
get(a) {
val _v1 = _value
if (_v1 ! == UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
return synchronized(lock) {
val _v2 = _value
if (_v2 ! == UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else{ val typedValue = initializer!! () _value = typedValue initializer =null
typedValue
}
}
}
Copy the code
In fact, the internal use of Volatile + synchronized double check.
conclusion
Today, I reviewed the single example model with you, and I hope you can have a review of the old and know the new harvest.
reference
www.kotlincn.net/docs/refere…
Bye bye
Thank you for your reading. If you study with me, you can pay attention to my public account — building blocks on the code ❤️❤️, a knowledge point every day to establish a complete knowledge system architecture. Here is a group of Android friends, welcome to join us