The Singleton Design Pattern is one of the most widely used design patterns in software development. It falls under the category of creational design patterns, which deal with object creation mechanisms.
The concept of a Singleton was first introduced by the Gang of Four (GoF) in their book, “Design Patterns: Elements of Reusable Object-Oriented Software”, published in 1994. The Singleton pattern is defined as a creational pattern that ensures that only one instance of a class is created and provides a global point of access to that instance.
The Singleton pattern was developed to address a common problem in software development: the need for a global, single instance of an object that can be accessed by all other objects in the system. This can be useful for managing resources that are expensive to create, such as database connections or network sockets, or for objects that must maintain a consistent state throughout the application, such as user preferences or application settings.
Implementation
There are multiple ways of creating singleton pattern but enum based implementation is considered to be the best.
A single-element enum type is the best way to implement a singleton
Joshua Bloch, Effective Java 2nd Edition p.18
But before that, lets look at various other implementations and their pitfalls.
What are we building?
Suppose we have a logging class that is used to log messages throughout an application. We want to ensure that only one instance of this logging class exists, to avoid having multiple log files or conflicting log messages. We can use the Singleton pattern to achieve this.
Key consideration to ensure single instance
- Private Constructors
- Parameterised constructors should be private to ensure it cannot be instantiated from outside class.
- default constructors should be explicitly created and marked private as java creates a default constructor and can be instantiated from outside of class. If the default constructor is private, java doesn’t allow a class to be inherited. Inheritance aspect can also be handled by making a class final.
- Serialization & deserialization
- Serialization and deserialization are the processes of converting an object from its in-memory representation to a format that can be stored or transmitted over a network, and then back to its in-memory representation.
- Serialization refers to the process of converting an object into a stream of bytes that can be written to a file or sent over a network.
- Deserialization, on the other hand, refers to the process of reading a stream of bytes and reconstructing the original object from it.
- An instance of a class can be duplicated if it is serializable by serializing an object and deserializing into another instance.
- This can be prevented using
volatile
keyword.
- Thread safety
- multiple threads might enter the instance initialization block and can potentially create multiple instances of same class.
- we should use java’s `
synchronized
` keyword to ensure thread-safety
- enum
- an enum is a special type of class that represents a fixed set of constants
- Enums are immutable, which means that once an enum value is created, its state cannot be modified.
- Enums provide type safety, which means that only the values defined in the enum can be used.
- Enums are serializable, BUT Enum singletons are serialization safe because Java guarantees that enum values are serialized and deserialized as their names, rather than as objects with their own state.When an enum is serialized, only its name is stored, and when it is deserialized, the enum value is reconstituted using the name. This means that the deserialized enum value is guaranteed to be the same object instance as the original, which ensures that the singleton property of the enum is preserved.
Eager Initialization
In this approach, the Singleton instance is created eagerly, that is, at the time of class loading. This ensures that the Singleton instance is always available, but can result in increased memory consumption if the Singleton object is large or if it is not always needed.
// Logger.java
public class Logger {
private static Logger instance = new Logger();
private Logger() {}
public static Logger getInstance() {
return instance;
}
public void log(String message) {
// Log the message to a file or console
}
}
// Application.java
public class Application {
public static void main(String[] args) {
Logger LOGGER = Logger.getInstance();
LOGGER.log("this is a logger");
}
}
JavaKey Notes:
- It will always be created irrespectively if it is needed or not.
- It is inheritance safe as it has private default constructor
- it is NOT thread-safe
- it is NOT serialization-safe
Lazy Initialization
This is the most common and simple way to implement Singleton pattern. In this approach, we create the Singleton instance only when it is needed, that is, on the first call to the getInstance()
method. This is achieved by checking if the instance is null and creating a new instance if it is.
// Logger.java
public class Logger {
private static Logger instance = null;
private Logger() {
// Initialize the logging system
}
public static Logger getInstance() {
if(instance == null) {
instance = new Logger();
}
return instance;
}
public void log(String message) {
// Log the message to a file or console
}
}
// Application.java
public class Application {
public static void main(String[] args) {
Logger LOGGER = Logger.getInstance();
LOGGER.log("this is a logger");
}
}
JavaKey Notes:
- Instance is created only if needed
- It cannot be inherited since it has private default constructor
- It is NOT thread safe
- it is NOT serialization-safe
Thread-safe Initialization
In the above two approaches, the Singleton instance is not thread-safe. That is, if multiple threads try to access the getInstance()
method at the same time, they may create multiple instances of the Singleton class. To avoid this, we can make the getInstance()
method synchronized, but this can have a negative impact on performance. Alternatively, we can use the “double-checked locking” pattern to ensure that the Singleton instance is thread-safe.
// Logger.java
public class Logger {
private static volatile Logger instance = null;
private Logger() {}
public static Logger getInstance() {
if(instance == null) {
synchronized (Logger.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public void log(String message) {
// Log the message to a file or console
}
}
// Application.java
public class Application {
public static void main(String[] args) {
Logger LOGGER = Logger.getInstance();
LOGGER.log("this is a logger");
}
}
JavaKey Notes:
- Instance is created only if needed
- It cannot be inherited since it has private default constructor
- It is thread safe
- it is NOT serialization-safe
Enum Singleton
In Java 5 and later versions, we can use an enum to implement the Singleton pattern. This approach is thread-safe, serialization-safe, is immutable and does not require any additional code to handle these cases.
// Logger.java
public enum Logger {
INSTANCE;
public void log(String message) {
System.out.println(message);
}
}
// Application.java
public class Application {
public static void main(String[] args) {
Logger LOGGER = Logger.INSTANCE;
LOGGER.log("this is a logger");
}
}
JavaUnit Tests
// Singleton.java
public enum Singleton {
INSTANCE;
private String message;
public void setMessage(String message) {
this.message = message;
}
public String getMessage() {
return this.message;
}
public Singleton getInstance() {
return INSTANCE;
}
}
// SingletonTest.java
public class SingletonTest {
private static final int NUM_THREADS = 100;
private static final int NUM_ITERATIONS = 100;
private static final CountDownLatch latch = new CountDownLatch(NUM_THREADS);
@Test
public void testGetInstance() {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
assertSame(s1, s2);
}
@Test
public void testSingletonBehavior() {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
s1.setMessage("Hello world!");
assertEquals(s2.getMessage(), "Hello world!");
}
@Test
public void testSingletonSerialization() throws IOException, ClassNotFoundException {
Singleton instance1 = Singleton.getInstance();
instance1.setMessage("Hello world!");
// Serialize the Singleton object
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(instance1);
oos.close();
baos.close();
// Deserialize the Singleton object
byte[] serializedSingleton = baos.toByteArray();
ByteArrayInputStream bais = new ByteArrayInputStream(serializedSingleton);
ObjectInputStream ois = new ObjectInputStream(bais);
Singleton instance2 = (Singleton) ois.readObject();
ois.close();
bais.close();
// Test that the deserialized Singleton object is the same as the original instance
assertSame(instance1, instance2);
assertEquals(instance1.getMessage(), instance2.getMessage());
}
/*
In this test, we create NUM_THREADS threads, each of which calls Singleton.getInstance()
NUM_ITERATIONS times.
We use a CountDownLatch to ensure that all threads have completed before we finish the test.
If the getInstance() method of the Singleton object is thread safe, then we should not
encounter any race conditions or synchronization issues, and the test should pass without
any errors.
*/
@Test
public void testSingletonThreadSafety() throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);
for (int i = 0; i < NUM_THREADS; i++) {
executor.execute(() -> {
for (int j = 0; j < NUM_ITERATIONS; j++) {
Singleton.getInstance();
}
latch.countDown();
});
}
latch.await();
executor.shutdown();
}
}
JavaUsecases
Here are some examples of actual production level usage of the Singleton Design Pattern:
- Logging Frameworks: Most logging frameworks, such as Log4j and SLF4J, use the Singleton pattern to ensure that there is only one instance of the logger object. This approach makes it easier to manage the logging output, reduce resource consumption, and prevent conflicts.
- Configuration Management: In many Java applications, there is a need to read configuration files at runtime. The Singleton pattern can be used to ensure that only one instance of the configuration manager class exists, preventing the configuration data from being read multiple times and improving performance.
- Database Connections: Establishing database connections can be a costly operation. The Singleton pattern can be used to ensure that only one instance of the database connection object is created, preventing unnecessary connections from being established and improving application performance.
- Caching Objects: Caching is a common technique used to improve the performance of applications. The Singleton pattern can be used to implement a cache manager that maintains a single instance of the cache object, ensuring that cached data is consistent across the application.
singleton in JVM
Singleton Pattern & Design Principles
Compliance
- Encapsulation: The Singleton pattern encapsulates the creation and management of a single instance of a class within the class itself, hiding the details of how the instance is created from the rest of the code.
- Separation of Concerns: The Singleton pattern separates the concerns of creating and managing a single instance of a class from the concerns of using that instance in the rest of the code, making it easier to maintain and modify both parts independently.
- Single Responsibility Principle: The Singleton pattern follows the Single Responsibility Principle by ensuring that a class has only one responsibility: to create and manage a single instance of itself.
Violation
- Coupling: The Singleton pattern can create tight coupling between the Singleton and other parts of the code. Other parts of the code may depend on the Singleton, making it harder to change or replace the Singleton in the future.
Downside of Singleton Design Pattern
- Global State: The Singleton pattern can introduce global state into an application, making it harder to reason about the behavior of the code. Global state can make it more difficult to understand how different components of the application interact with each other, leading to bugs and issues down the line.
- Testing: Testing Singleton objects can be difficult since they cannot be easily instantiated or substituted with mocks during testing. This can make it harder to write automated tests for the components that depend on the Singleton.
- Thread Safety: If not implemented properly, the Singleton pattern can lead to race conditions and thread safety issues. In a multi-threaded environment, two or more threads may try to create an instance of the Singleton at the same time, resulting in multiple instances being created.
- Dependency Injection: The Singleton pattern can make it more difficult to use dependency injection frameworks, such as Spring or Guice, since they rely on creating and managing instances of objects themselves.
- Tight Coupling: The Singleton pattern can create tight coupling between the Singleton and other parts of the code. Other parts of the code may depend on the Singleton, making it harder to change or replace the Singleton in the future.
Comments
One response to “Mastering the Singleton Design Pattern in Java”
very good