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@1517365bSingleton@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: 12345678Thread-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
Post a Comment