[Design Pattern] Lesson 03: Using Singleton in Java
This article delves into the Singleton Design Pattern in Java, elucidating its concept and implementation methods. It highlights Singleton's purpose to ensure a single class instance throughout an application and explores various implementation techniques, including eager, lazy, and Bill Pugh Singleton Initialization. Ideal for developers seeking to deepen their understanding of design patterns in Java. For more details, you can read the full article.
What is Singleton Design Pattern?
Before we delve into the concept of the Singleton Design Pattern, let's together look at this practical example. In recent years, Lombok has emerged as a deity in the programming world, a fantastic library to reduce boilerplate code. Most coders have also used snippets of code like the ones below.
@Slf4j
public class DashboardController{
@GetMapping
public ResponseEntity>> getAll() {
log.info("Processing for dashboard");
return ResponseEntity.ok(ResponseDto.response(Map.of()));
}
}
I firmly believe that many Devs also question where this log object comes from? How many instances of the log are created in the application... From what I understand, this log object is created by Lombok through the @Slf4j annotation. And the special thing I want to emphasize here is that there is only one instance of the log object created throughout the entire application. And of course, this instance can be used in any class of the program.
So, ensuring that only one instance of a class is created throughout the entire program and ensuring that all classes can use this instance is the noble and crucial task of the Singleton Design Pattern.
Singleton Design Pattern Implementation
There are several ways to implement a Singleton Design Pattern, but most methods adhere to the following rules:
- First: The constructor must be private to prevent the instantiation of the class from outside.
- Second: There must be a public static method to return the instance of the class. This method is commonly named getInstance().
- Public to ensure other classes can access the instance.
- Static so other classes can use the getInstance() method without needing to instantiate the object. If one had to instantiate the object to use the getInstance() method, it would defeat the purpose of a Singleton.
- Third: There must be a private static variable; this is the representation of the Singleton.
- Private to prevent access from outside.
- Static because this variable is used within the static getInstance() method.
Now, let's explore the ways to implement the Singleton Design Pattern together.
Eager Initialization
This is the simplest way to implement a Singleton in the Java programming language.
public class DatabaseConnection{
private static DatabaseConnection instance = new DatabaseConnection();
private DatabaseConnection(){ /*hidden constructor*/ }
private static DatabaseConnection getInstance(){
return instance;
}
}
However, the downside of this approach is that the instance is always instantiated during class loading. This can lead to wastefulness since there may be no clients using it. Moreover, this instantiation method cannot apply exception handling.
To address the possibility of handling exceptions during the instance initialization process, let's look at the Static block initialization solution.
Static Block Initialization
With a static block, we can handle exceptions that occur during the instance initialization process. However, it still does not improve the situation of instantiating the instance upon class loading.
public class DatabaseConnection{
private static DatabaseConnection instance;
private DatabaseConnection(){ /*hidden constructor*/ }
static {
try{
instance = new DatabaseConnection();
}catch(Exception e){
throw new RuntimeException("Error");
}
}
private static DatabaseConnection getInstance(){
return instance;
}
}
Lazy Initialization
Lazy initialization allows us to instantiate the instance directly within the getInstance() method. This ensures that the instance is only created when it is actually used by a client.
public class DatabaseConnection{
private static DatabaseConnection instance;
private DatabaseConnection(){ /*hidden constructor*/ }
private static DatabaseConnection getInstance(){
if(instance != null){
try{
instance = new DatabaseConnection();
}catch(Exception e){
throw new RuntimeException("Error");
}
}
return instance;
}
}
This method works well in a single-threaded environment. However, in a multi-threading environment, there's a possibility that multiple threads might call the getInstance() method at the same time. This could lead to more than one instance being created.
The simplest way to resolve this issue is to use the synchronized keyword for the getInstance() method.
public class DatabaseConnection{
private static DatabaseConnection instance;
private DatabaseConnection(){ /*hidden constructor*/ }
private static synchronized DatabaseConnection getInstance(){
if(instance != null){
try{
instance = new DatabaseConnection();
}catch(Exception e){
throw new RuntimeException("Error");
}
}
return instance;
}
}
Synchronized ensures that only one thread can access the getInstance() method at a time. However, this also means a reduction in the application's performance since other threads must wait until the current thread releases the getInstance() method. Moreover, before creating an instance, there are many tasks to do such as validation, logging… these tasks do not necessarily need to be synchronized.
We can improve this by moving synchronized inside the getInstance() method.
private static DatabaseConnection getInstance(){
if(instance != null){
synchronized(DatabaseConnection.class){
if(instance != null){
try{
instance = new DatabaseConnection();
}catch(Exception e){
throw new RuntimeException("Error");
}
}
}
}
return instance;
}
Pay attention here, loves, within the synchronized block, we need to check for null one more time to ensure that no thread creates an instance while waiting.
Bill Pugh Singleton
Bill Pugh employs a static nested class and initializes the Singleton's instance within this nested class. This leverages the JVM's class loading mechanism to ensure that the instance is created safely when the Singleton class is loaded, and we do not need to use the synchronized keyword, significantly improving performance.
public class DatabaseConnection{
private DatabaseConnection(){/*hidden constructor */}
private static class SingletonHelper{
private static final DatabaseConnection instance = new DatabaseConnection();
}
private static DatabaseConnection getInstance(){
return SingletonHelper.instance;
}
}
Breaking Singleton
There are two ways to break the structure of a Singleton: Java Reflection and Deserialization.
Using Java Reflection to Break the Structure of a Singleton
public class SingletonTest {
public static void main(String[] args) {
DatabaseConnection instanceOne = DatabaseConnection.getInstance();
DatabaseConnection instanceTwo = null;
try {
Constructor[] constructors = DatabaseConnection.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
constructor.setAccessible(true);
instanceTwo = (DatabaseConnection) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
}
The hashCode results obtained are completely different. To avoid the structure of a Singleton being broken by Reflection, we can use an Enum to create a Singleton.
public enum EnumSingleton {
INSTANCE;
public static void doSomething() {
// implement TODO
}
}
Using Deserialization to Break the Structure of a Singleton
We need to implement the Serializable interface in the Singleton class to be able to store its state in a file system and then retrieve it for further processing.
public class DatabaseConnection implements Serializable{
private static DatabaseConnection instance = new DatabaseConnection();
private DatabaseConnection(){ /*hidden constructor*/ }
private static DatabaseConnection getInstance(){
return instance;
}
}
However, when we deserialize, it creates a new object of the Singleton class. Clearly, we then have two different objects of the same Singleton class.
public class SingletonSerializedTest {
public static void main(String[] args) throws FileNotFoundException,
IOException, ClassNotFoundException {
DatabaseConnection instanceOne = DatabaseConnection.getInstance();
ObjectOutput outObj = new ObjectOutputStream(new FileOutputStream("test.bin"));
outObj.writeObject(instanceOne);
outObj.close();
ObjectInput inObj = new ObjectInputStream(new FileInputStream("test.bin"));
DatabaseConnection instanceTwo = (DatabaseConnection) inObj.readObject();
inObj.close();
System.out.println("instanceOne hashCode="+instanceOne.hashCode());
System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());
}
}
To handle this situation, we need to implement the readResolve() method in the Singleton class.
public class DatabaseConnection implements Serializable{
private DatabaseConnection(){/*hidden constructor */}
private static class SingletonHelper{
private static final DatabaseConnection instance = new DatabaseConnection();
}
private static DatabaseConnection getInstance(){
return SingletonHelper.instance;
}
protected Object readResolve() {
return getInstance();
}
}
I would like to conclude the article here. Through this article, I have listed what a Singleton Design Pattern is and the ways to implement a Singleton… I hope to receive many contributions from everyone so that the programming community can continue to develop and, especially, so that future articles can be improved.
Our dedicated team is adept at implementing sophisticated design patterns, including Singleton, to ensure your software is robust, scalable, and efficient. By choosing Saigon Technology, you leverage a wealth of knowledge and experience in delivering high-quality Java development services. Let's embark on a journey of technological excellence together and bring your innovative ideas to life with the best practices in the industry.