Java Singleton Design Pattern (Creational Pattern)

Mastering the Singleton Design Pattern in Java

Introduction

The Singleton Design Pattern ensures that a class has only one instance and provides a global point of access to that instance. It is part of the creational design patterns and is widely used in scenarios where a single object needs to coordinate actions across the system.


Implementing the Singleton Pattern

Let's look at a basic implementation of the Singleton pattern.

public class Singleton {
private static volatile Singleton instance; private String data; private Singleton(String data) { this.data = data; } public static Singleton getInstance(String data) { Singleton result = instance; if (result == null) { synchronized (Singleton.class) { result = instance; if (result == null) { instance = result = new Singleton(data); } } } return result; } public String getData() { return data; } public void setData(String data) { this.data = data; } }

Explanation:

  • Private Constructor: Prevents instantiation from outside the class.
  • Volatile Instance: Ensures visibility of changes to variables across threads.
  • Double-Checked Locking: Minimizes synchronization overhead by locking only when the instance is null.
  • Getter and Setter: Methods to access and modify the data field.

Main Method to Test Singleton

public class SingletonMain {
public static void main(String[] args) { Singleton instance1 = Singleton.getInstance("Initial data"); System.out.println(instance1); // Output: Singleton{data='Initial data'} // Updating the data instance1.setData("Updated data"); System.out.println(instance1); // Output: Singleton{data='Updated data'} Singleton instance2 = Singleton.getInstance("Another data"); System.out.println(instance2); // Output: Singleton{data='Updated data'} System.out.println(instance1 == instance2); // should print true } }


Handling Serialization Issues

Serialization can break the Singleton pattern by creating a new instance upon deserialization. To resolve this, implement the readResolve method:


import java.io.ObjectStreamException; import java.io.Serializable; public class Singleton implements Serializable { private static volatile Singleton instance; private String data; private Singleton(String data) { this.data = data; } public static Singleton getInstance(String data) { Singleton result = instance; if (result == null) { synchronized (Singleton.class) { result = instance; if (result == null) { instance = result = new Singleton(data); } } } return result; } private Object readResolve() throws ObjectStreamException { return getInstance(data); } }

Explanation:

  • readResolve Method: Ensures that the deserialized object returns the existing instance.

Main Method for Serialization Test

import java.io.*; public class SingletonSerializationTest { public static void main(String[] args) { try { Singleton instance1 = Singleton.getInstance("data"); // Serialize the instance ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.ser")); out.writeObject(instance1); out.close(); // Deserialize the instance ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.ser")); Singleton instance2 = (Singleton) in.readObject(); in.close(); System.out.println(instance1); System.out.println(instance2); System.out.println(instance1 == instance2); // should print true } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } }

Expected Output:

Singleton@1517365b
Singleton@1517365b true


Preventing Singleton Breakage via Reflection

Reflection can break the Singleton pattern by accessing the private constructor. Prevent this by throwing an exception if an instance already exists:

public class Singleton {
private static volatile Singleton instance; private String data; private Singleton(String data) { if (instance != null) { throw new RuntimeException("Use getInstance() method to get the single instance of this class."); } this.data = data; } public static Singleton getInstance(String data) { Singleton result = instance; if (result == null) { synchronized (Singleton.class) { result = instance; if (result == null) { instance = result = new Singleton(data); } } } return result; } public String getData() { return data; } public void setData(String data) { this.data = data; } }

Explanation:

  • RuntimeException in Constructor: Prevents instantiation via reflection.

Main Method for Reflection Test

import java.lang.reflect.Constructor;
public class SingletonReflectionTest { public static void main(String[] args) { try { Singleton instance1 = Singleton.getInstance("data"); Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(String.class); constructor.setAccessible(true); Singleton instance2 = constructor.newInstance("data"); } catch (Exception e) { System.out.println(e.getMessage()); } } }

Expected Output:

Use getInstance() method to get the single instance of this class.


Multithreading in Singleton Pattern

Let's test our Singleton implementation in a multithreaded environment:

public class SingletonThreadTest {
public static void main(String[] args) { Runnable task = () -> { Singleton singleton = Singleton.getInstance("data"); System.out.println(Thread.currentThread().getName() + ": " + singleton.hashCode()); }; Thread thread1 = new Thread(task); Thread thread2 = new Thread(task); Thread thread3 = new Thread(task); thread1.start(); thread2.start(); thread3.start(); } }

Expected Output:

Thread-0: 12345678
Thread-1: 12345678 Thread-2: 12345678


Class Diagram of Singleton Creational Pattern


Usecase of Singleton Design Pattern:

Database Connection Pool

Managing database connections efficiently is crucial for the performance of an application. A connection pool ensures that multiple connections to the database are reused rather than created and destroyed repeatedly. A Singleton pattern can be used to manage the connection pool.

Example:

import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; public class ConnectionPool { private static volatile ConnectionPool instance; private List<Connection> availableConnections = new ArrayList<>(); private static final int MAX_CONNECTIONS = 10; private ConnectionPool() { // Initialize the connection pool with connections for (int i = 0; i < MAX_CONNECTIONS; i++) { try { availableConnections.add(DriverManager.getConnection("jdbc:yourdatabaseurl", "username", "password")); } catch (SQLException e) { e.printStackTrace(); } } } public static ConnectionPool getInstance() { if (instance == null) { synchronized (ConnectionPool.class) { if (instance == null) { instance = new ConnectionPool(); } } } return instance; } public synchronized Connection getConnection() { if (availableConnections.isEmpty()) { throw new RuntimeException("No available connections"); } return availableConnections.remove(0); } public synchronized void releaseConnection(Connection connection) { availableConnections.add(connection); } } // Usage public class Main { public static void main(String[] args) { ConnectionPool pool = ConnectionPool.getInstance(); Connection conn = pool.getConnection(); // Use the connection // ... pool.releaseConnection(conn); } }

Above example illustrate how the Singleton pattern can be applied in realtime scenario to ensure only one instance of a class is created and shared across the application, providing a global point of access.


Conclusion

The Singleton Design Pattern is a powerful tool for ensuring a class has only one instance and provides a global point of access to it. By handling potential issues like serialization and reflection, and ensuring thread safety, we can implement a robust Singleton pattern in Java.


I hope you found this post insightful.
For more design patterns, visit https://techinsightswithamrish.blogspot.com.

Comments

Popular posts from this blog

Java Builder Design Pattern - (Creational Pattern)

Mastering Java Design Patterns: A Comprehensive Guide with Real-World Examples

Java Design Pattern - Creational - Abstract Factory Pattern