In this article, we will understand the Singleton class, the pros and cons of using Singleton, and finally, the ways of writing a Singleton class in Kotlin.
What is Singleton?
Singleton is a creational design pattern that ensures a single instance of a class for the lifetime of an application.
Pros
- Global Point of Access: Makes the Singleton class easy to access.
- Reduced Memory Usage: Avoids repeated allocation and deallocation of memory, reducing the garbage collector’s overhead.
Cons
- Tight Coupling and Difficult Testing: Can create dependencies, making the application hard to test and modify.
- Thread Safety and Memory Leaks: If not handled properly, it can lead to problems with thread safety and memory leaks.
Ways to write Singleton:
Double-Checked locking method (Lazy Initialization)
In double-checked locking, we check for an existing instance of the Singleton class twice, before and after entering the synchronized block, ensuring that no more than one instance of Singleton gets created.
class Singleton private constructor() {
companion object {
@Volatile
private var instance: Singleton? = null
fun getInstance(): Singleton {
if(instance==null) { //Check 1
synchronized(this) {
if(instance==null) {//Check 2
instance = Singleton()
}
}
}
return instance!!
}
}
}
Explanation:
- Private constructor: Ensures that the constructor can only be accessed within the class.
- Synchronized block: Ensures that only one thread can enter the block at a time, preventing multiple instances from being created.
-
Companion object (can be used without creating instance of a class): Used to create the Singleton class. It contains a static method (
getInstance()
) which returns the single instance of the class. - Volatile Keyword: Ensures that the instance is immediately visible to other threads after creation.
-
Check 1: If we do not add this check, every time
getInstance()
is called thread(s) have to enter synchronized block which will cause performance overhead by blocking other threads. -
Check 2: As multiple thread can go in waiting state on synchronized block(but already entered the
getInstance()
method), if one thread creates the instance and it is visible to all the threads immediately (using volatile), this check will prevent creation of instance again.
Object keyword in Kotlin (Eager Initialization)
There is one more easy way to create a singleton — Object
.
Let’s check the example
object Config {
const val TAG = "Config"
}
Then why do we even need the double-checked locking method?
Let’s decompile the class to understand how the instance is created internally:
public final class Config {
@NotNull
public static final String TAG = "Config";
@NotNull
public static final Config INSTANCE;
private Config() {
}
static {
Config var0 = new Config();
INSTANCE = var0;
}
}
As we can see, when decompiled, it uses a static block to create the instance. The static block in Java runs as soon as the class is loaded. The JVM internally loads classes lazily, ensuring thread safety.
Difference between both methods:
- Object Keyword: Creates the instance as soon as the class is loaded. (Eager Initialization).
-
Double-Checked Locking: Creates the instance only when
getInstance()
is called, even if the class is already loaded (Lazy Initialization).
Which method should we use?
Both methods work similarly in creating a Singleton and can be used if handled properly.
Things to consider:
- With the
object
expression, if the class is loaded by mistake and never used, the instance will still occupy memory throughout the application’s lifecycle.
object Config {
const val TAG = "Config"
}
val nameConfig = Config.javaClass.simpleName //instance of Config will be created
- We cannot pass arguments to object class as it does not have a constructor, but we can when creating it manually.
class Singleton private constructor(tag: String) {
..
fun getInstance(tag: String): Singleton {
..
if(instance == null) {
instance = Singleton(tag)
}
..
return instance!!
}
}
Bonus: Companion Object
class Demo {
companion object DemoConfig {
const val TAG = "DemoConfig"
}
}
fun main() {
val nameDemo = Demo //instance of DemoConfig will be created, Demo instance will not be created
}
The companion object is equivalent to the static
keyword in Java. It is associated with the class and is instantiated as soon as the containing class is loaded, even if the companion object itself is not used. We can use the companion object without instantiating the corresponding class.
Source Code: GitHub
Happy Coding ✌️
Top comments (0)