Design Patterns: Singleton in Java

Introduction

The Singleton pattern ensures a class has only one instance and provides a global point of access to it. This is useful when exactly one object is needed to coordinate actions across the system, such as configuration managers, logging services, or database connections.

Classic Implementation Approaches

1. Eager Initialization

/**
* Eager initialization - instance created at class loading time
* Simple but may waste resources if instance is never used
*/
public class EagerSingleton {
// Private static instance created eagerly
private static final EagerSingleton INSTANCE = new EagerSingleton();
// Private constructor to prevent instantiation
private EagerSingleton() {
// Prevent reflection attacks
if (INSTANCE != null) {
throw new IllegalStateException("Instance already created");
}
System.out.println("EagerSingleton instance created");
}
// Public method to provide access to singleton instance
public static EagerSingleton getInstance() {
return INSTANCE;
}
// Business methods
public void doSomething() {
System.out.println("EagerSingleton doing something");
}
}

2. Static Block Initialization

/**
* Static block initialization - similar to eager but allows exception handling
*/
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
// Static block for initialization
static {
try {
instance = new StaticBlockSingleton();
} catch (Exception e) {
throw new RuntimeException("Exception occurred in singleton creation", e);
}
}
private StaticBlockSingleton() {
System.out.println("StaticBlockSingleton instance created");
}
public static StaticBlockSingleton getInstance() {
return instance;
}
public void doSomething() {
System.out.println("StaticBlockSingleton doing something");
}
}

3. Lazy Initialization

/**
* Lazy initialization - creates instance only when needed
* Not thread-safe in multithreaded environments
*/
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
System.out.println("LazySingleton instance created");
}
// Not thread-safe!
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
public void doSomething() {
System.out.println("LazySingleton doing something");
}
}

Thread-Safe Implementations

4. Thread-Safe with Synchronized Method

/**
* Thread-safe singleton with synchronized method
* Provides thread safety but poor performance due to locking
*/
public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton() {
System.out.println("ThreadSafeSingleton instance created");
}
// Synchronized method - thread safe but performance overhead
public static synchronized ThreadSafeSingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
return instance;
}
public void doSomething() {
System.out.println("ThreadSafeSingleton doing something");
}
}

5. Double-Checked Locking

/**
* Double-Checked Locking - reduces synchronization overhead
* Uses volatile keyword for proper visibility in multithreaded environments
*/
public class DoubleCheckedLockingSingleton {
// volatile ensures multiple threads handle instance correctly
private static volatile DoubleCheckedLockingSingleton instance;
private DoubleCheckedLockingSingleton() {
// Prevent reflection attacks
if (instance != null) {
throw new IllegalStateException("Instance already created");
}
System.out.println("DoubleCheckedLockingSingleton instance created");
}
public static DoubleCheckedLockingSingleton getInstance() {
// First check (without synchronization)
if (instance == null) {
// Synchronize only when instance is null
synchronized (DoubleCheckedLockingSingleton.class) {
// Second check (with synchronization)
if (instance == null) {
instance = new DoubleCheckedLockingSingleton();
}
}
}
return instance;
}
public void doSomething() {
System.out.println("DoubleCheckedLockingSingleton doing something");
}
}

6. Bill Pugh Singleton (Initialization-on-demand Holder Idiom)

/**
* Bill Pugh Singleton - most widely used approach
* Thread-safe without synchronization overhead
* Uses static inner helper class
*/
public class BillPughSingleton {
private BillPughSingleton() {
System.out.println("BillPughSingleton instance created");
}
// Private static inner class that contains the instance
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
public void doSomething() {
System.out.println("BillPughSingleton doing something");
}
}

Enum Singleton (Recommended Approach)

7. Enum Singleton

/**
* Enum Singleton - Joshua Bloch's recommended approach
* Handles serialization and reflection attacks automatically
* Most secure and simple implementation
*/
public enum EnumSingleton {
INSTANCE;
// Instance variables
private String data;
private int counter;
// Constructor (implicitly private)
EnumSingleton() {
System.out.println("EnumSingleton instance created");
this.data = "Default Data";
this.counter = 0;
}
// Business methods
public void doSomething() {
System.out.println("EnumSingleton doing something");
counter++;
}
// Accessor methods
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public int getCounter() {
return counter;
}
// Static factory method (alternative to INSTANCE)
public static EnumSingleton getInstance() {
return INSTANCE;
}
}

Real-World Singleton Examples

1. Configuration Manager

/**
* Configuration Manager Singleton - manages application configuration
*/
public class ConfigurationManager {
private static volatile ConfigurationManager instance;
private Properties properties;
private ConfigurationManager() {
loadConfiguration();
}
public static ConfigurationManager getInstance() {
if (instance == null) {
synchronized (ConfigurationManager.class) {
if (instance == null) {
instance = new ConfigurationManager();
}
}
}
return instance;
}
private void loadConfiguration() {
properties = new Properties();
try (InputStream input = getClass().getClassLoader()
.getResourceAsStream("config.properties")) {
if (input != null) {
properties.load(input);
} else {
System.out.println("Config file not found, using defaults");
setDefaultProperties();
}
} catch (IOException e) {
System.out.println("Error loading configuration: " + e.getMessage());
setDefaultProperties();
}
}
private void setDefaultProperties() {
properties.setProperty("database.url", "jdbc:mysql://localhost:3306/mydb");
properties.setProperty("database.username", "admin");
properties.setProperty("database.password", "password");
properties.setProperty("app.name", "My Application");
properties.setProperty("app.version", "1.0.0");
}
public String getProperty(String key) {
return properties.getProperty(key);
}
public String getProperty(String key, String defaultValue) {
return properties.getProperty(key, defaultValue);
}
public int getIntProperty(String key, int defaultValue) {
try {
return Integer.parseInt(properties.getProperty(key));
} catch (NumberFormatException e) {
return defaultValue;
}
}
public boolean getBooleanProperty(String key, boolean defaultValue) {
String value = properties.getProperty(key);
if (value != null) {
return Boolean.parseBoolean(value);
}
return defaultValue;
}
// Reload configuration (useful for hot-reloading)
public synchronized void reloadConfiguration() {
properties.clear();
loadConfiguration();
}
}

2. Logger Service

/**
* Logger Service Singleton - handles application logging
*/
public class LoggerService {
private static volatile LoggerService instance;
private PrintWriter logWriter;
private LogLevel currentLogLevel;
public enum LogLevel {
DEBUG, INFO, WARN, ERROR
}
private LoggerService() {
initializeLogger();
this.currentLogLevel = LogLevel.INFO;
}
public static LoggerService getInstance() {
if (instance == null) {
synchronized (LoggerService.class) {
if (instance == null) {
instance = new LoggerService();
}
}
}
return instance;
}
private void initializeLogger() {
try {
// Create logs directory if it doesn't exist
File logsDir = new File("logs");
if (!logsDir.exists()) {
logsDir.mkdirs();
}
// Create log file with timestamp
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
File logFile = new File(logsDir, "application_" + timestamp + ".log");
logWriter = new PrintWriter(new FileWriter(logFile, true), true);
log("INFO", "Logger initialized successfully");
} catch (IOException e) {
System.err.println("Failed to initialize logger: " + e.getMessage());
// Fallback to console
logWriter = new PrintWriter(System.out, true);
}
}
public void setLogLevel(LogLevel level) {
this.currentLogLevel = level;
log("INFO", "Log level changed to: " + level);
}
public void debug(String message) {
if (currentLogLevel.ordinal() <= LogLevel.DEBUG.ordinal()) {
log("DEBUG", message);
}
}
public void info(String message) {
if (currentLogLevel.ordinal() <= LogLevel.INFO.ordinal()) {
log("INFO", message);
}
}
public void warn(String message) {
if (currentLogLevel.ordinal() <= LogLevel.WARN.ordinal()) {
log("WARN", message);
}
}
public void error(String message) {
if (currentLogLevel.ordinal() <= LogLevel.ERROR.ordinal()) {
log("ERROR", message);
}
}
public void error(String message, Throwable throwable) {
if (currentLogLevel.ordinal() <= LogLevel.ERROR.ordinal()) {
log("ERROR", message + " - " + throwable.getMessage());
throwable.printStackTrace(logWriter);
}
}
private void log(String level, String message) {
String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date());
String logEntry = String.format("[%s] %s - %s", timestamp, level, message);
logWriter.println(logEntry);
// Also print to console for ERROR level
if ("ERROR".equals(level)) {
System.err.println(logEntry);
}
}
public void close() {
if (logWriter != null) {
log("INFO", "Logger shutting down");
logWriter.close();
}
}
}

3. Database Connection Pool

/**
* Database Connection Pool Singleton - manages database connections
*/
public class DatabaseConnectionPool {
private static volatile DatabaseConnectionPool instance;
private final List<Connection> availableConnections;
private final List<Connection> usedConnections;
private final int maxConnections;
private DatabaseConnectionPool() {
this.maxConnections = 10;
this.availableConnections = new ArrayList<>();
this.usedConnections = new ArrayList<>();
initializePool();
}
public static DatabaseConnectionPool getInstance() {
if (instance == null) {
synchronized (DatabaseConnectionPool.class) {
if (instance == null) {
instance = new DatabaseConnectionPool();
}
}
}
return instance;
}
private void initializePool() {
try {
for (int i = 0; i < maxConnections; i++) {
Connection connection = createConnection();
availableConnections.add(connection);
}
System.out.println("Database connection pool initialized with " + 
availableConnections.size() + " connections");
} catch (SQLException e) {
throw new RuntimeException("Failed to initialize connection pool", e);
}
}
private Connection createConnection() throws SQLException {
// In real application, use connection string from configuration
String url = "jdbc:mysql://localhost:3306/mydb";
String username = "username";
String password = "password";
return DriverManager.getConnection(url, username, password);
}
public synchronized Connection getConnection() throws SQLException {
if (availableConnections.isEmpty()) {
if (usedConnections.size() < maxConnections) {
// Create new connection if under max limit
Connection newConnection = createConnection();
usedConnections.add(newConnection);
return newConnection;
} else {
throw new SQLException("No available connections in pool");
}
}
Connection connection = availableConnections.remove(availableConnections.size() - 1);
usedConnections.add(connection);
// Test if connection is still valid
if (!connection.isValid(2)) {
connection.close();
connection = createConnection();
}
return connection;
}
public synchronized boolean releaseConnection(Connection connection) {
usedConnections.remove(connection);
try {
if (!connection.isClosed() && connection.isValid(2)) {
availableConnections.add(connection);
return true;
} else {
connection.close();
return false;
}
} catch (SQLException e) {
System.err.println("Error releasing connection: " + e.getMessage());
return false;
}
}
public synchronized void shutdown() {
// Close all connections
availableConnections.forEach(this::closeConnection);
usedConnections.forEach(this::closeConnection);
availableConnections.clear();
usedConnections.clear();
System.out.println("Database connection pool shutdown completed");
}
private void closeConnection(Connection connection) {
try {
if (connection != null && !connection.isClosed()) {
connection.close();
}
} catch (SQLException e) {
System.err.println("Error closing connection: " + e.getMessage());
}
}
public int getAvailableConnectionsCount() {
return availableConnections.size();
}
public int getUsedConnectionsCount() {
return usedConnections.size();
}
}

Testing Singleton Patterns

Comprehensive Test Suite

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import static org.junit.jupiter.api.Assertions.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class SingletonTest {
@Test
public void testEagerSingleton() {
EagerSingleton instance1 = EagerSingleton.getInstance();
EagerSingleton instance2 = EagerSingleton.getInstance();
assertSame(instance1, instance2, "Both instances should be the same");
instance1.doSomething();
}
@Test
public void testThreadSafeSingleton() throws InterruptedException, ExecutionException {
final int numThreads = 10;
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
List<Future<ThreadSafeSingleton>> futures = new ArrayList<>();
// Create multiple threads trying to get singleton instance
for (int i = 0; i < numThreads; i++) {
futures.add(executor.submit(ThreadSafeSingleton::getInstance));
}
// Verify all threads got the same instance
ThreadSafeSingleton firstInstance = futures.get(0).get();
for (Future<ThreadSafeSingleton> future : futures) {
assertSame(firstInstance, future.get(), "All instances should be the same");
}
executor.shutdown();
}
@Test
public void testDoubleCheckedLockingMultithreaded() throws InterruptedException, ExecutionException {
final int numThreads = 20;
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
List<Future<DoubleCheckedLockingSingleton>> futures = new ArrayList<>();
for (int i = 0; i < numThreads; i++) {
futures.add(executor.submit(DoubleCheckedLockingSingleton::getInstance));
}
DoubleCheckedLockingSingleton firstInstance = futures.get(0).get();
for (Future<DoubleCheckedLockingSingleton> future : futures) {
assertSame(firstInstance, future.get(), "All instances should be the same in multithreaded environment");
}
executor.shutdown();
}
@Test
public void testBillPughSingleton() {
BillPughSingleton instance1 = BillPughSingleton.getInstance();
BillPughSingleton instance2 = BillPughSingleton.getInstance();
assertSame(instance1, instance2, "Bill Pugh Singleton instances should be identical");
instance1.doSomething();
}
@Test
public void testEnumSingleton() {
EnumSingleton instance1 = EnumSingleton.INSTANCE;
EnumSingleton instance2 = EnumSingleton.getInstance();
EnumSingleton instance3 = EnumSingleton.INSTANCE;
assertSame(instance1, instance2, "Enum singleton instances should be identical");
assertSame(instance1, instance3, "Enum singleton instances should be identical");
// Test business methods
instance1.doSomething();
assertEquals(1, instance1.getCounter());
instance2.doSomething();
assertEquals(2, instance2.getCounter());
}
@Test
public void testReflectionAttackPrevention() {
// Test that reflection cannot create new instances
assertThrows(IllegalStateException.class, () -> {
Constructor<DoubleCheckedLockingSingleton> constructor = 
DoubleCheckedLockingSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
constructor.newInstance();
});
}
@Test
public void testConfigurationManager() {
ConfigurationManager config1 = ConfigurationManager.getInstance();
ConfigurationManager config2 = ConfigurationManager.getInstance();
assertSame(config1, config2, "Configuration manager should be singleton");
// Test configuration properties
String appName = config1.getProperty("app.name");
assertNotNull(appName);
String defaultValue = config1.getProperty("nonexistent.property", "default");
assertEquals("default", defaultValue);
int intValue = config1.getIntProperty("nonexistent.int", 42);
assertEquals(42, intValue);
}
@Test
public void testLoggerService() {
LoggerService logger1 = LoggerService.getInstance();
LoggerService logger2 = LoggerService.getInstance();
assertSame(logger1, logger2, "Logger service should be singleton");
// Test logging at different levels
logger1.setLogLevel(LoggerService.LogLevel.DEBUG);
logger1.debug("Debug message");
logger1.info("Info message");
logger1.warn("Warning message");
logger1.error("Error message");
// Test error with exception
Exception testException = new RuntimeException("Test exception");
logger1.error("Error with exception", testException);
}
@Test
public void testDatabaseConnectionPool() throws SQLException {
DatabaseConnectionPool pool1 = DatabaseConnectionPool.getInstance();
DatabaseConnectionPool pool2 = DatabaseConnectionPool.getInstance();
assertSame(pool1, pool2, "Database connection pool should be singleton");
// Test connection management
Connection conn1 = pool1.getConnection();
assertNotNull(conn1);
assertFalse(conn1.isClosed());
assertEquals(1, pool1.getUsedConnectionsCount());
boolean released = pool1.releaseConnection(conn1);
assertTrue(released);
assertEquals(0, pool1.getUsedConnectionsCount());
}
}

Best Practices and Considerations

When to Use Singleton

public class SingletonBestPractices {
/**
* Appropriate use cases for Singleton:
* 1. Logging services
* 2. Configuration managers
* 3. Database connection pools
* 4. Hardware access (printers, GPIO)
* 5. Caching mechanisms
* 6. Thread pools
* 7. Service locators in DI containers
*/
// Good example: Cache Manager
public static class CacheManager {
private static volatile CacheManager instance;
private final Map<String, Object> cache;
private CacheManager() {
this.cache = new ConcurrentHashMap<>();
}
public static CacheManager getInstance() {
if (instance == null) {
synchronized (CacheManager.class) {
if (instance == null) {
instance = new CacheManager();
}
}
}
return instance;
}
public void put(String key, Object value) {
cache.put(key, value);
}
public Object get(String key) {
return cache.get(key);
}
public void remove(String key) {
cache.remove(key);
}
public void clear() {
cache.clear();
}
}
// Good example: Thread Pool Manager
public static class ThreadPoolManager {
private static volatile ThreadPoolManager instance;
private final ExecutorService executorService;
private ThreadPoolManager() {
this.executorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
}
public static ThreadPoolManager getInstance() {
if (instance == null) {
synchronized (ThreadPoolManager.class) {
if (instance == null) {
instance = new ThreadPoolManager();
}
}
}
return instance;
}
public Future<?> submit(Runnable task) {
return executorService.submit(task);
}
public <T> Future<T> submit(Callable<T> task) {
return executorService.submit(task);
}
public void shutdown() {
executorService.shutdown();
}
}
}
/**
* Anti-patterns and when NOT to use Singleton:
* 1. When you need multiple instances with different configurations
* 2. When testing becomes difficult due to global state
* 3. When it creates hidden dependencies
* 4. When it violates Single Responsibility Principle
* 5. When it makes code less flexible and harder to extend
*/
public class SingletonAntiPatterns {
// Bad: Using singleton for business logic that might need multiple instances
public static class BadPaymentProcessor {
private static BadPaymentProcessor instance;
private BadPaymentProcessor() {}
public static BadPaymentProcessor getInstance() {
if (instance == null) {
instance = new BadPaymentProcessor();
}
return instance;
}
// This should not be singleton as different payment methods
// might need different processors
public void processPayment() {
// Payment processing logic
}
}
// Better: Use dependency injection instead
public interface PaymentProcessor {
void processPayment();
}
public static class CreditCardProcessor implements PaymentProcessor {
@Override
public void processPayment() {
// Credit card specific logic
}
}
public static class PayPalProcessor implements PaymentProcessor {
@Override
public void processPayment() {
// PayPal specific logic
}
}
}

Advanced Singleton Patterns

1. Singleton with Registry

/**
* Singleton with Registry - manages multiple singleton instances
*/
public class SingletonRegistry {
private static final Map<String, Object> REGISTRY = new ConcurrentHashMap<>();
private SingletonRegistry() {
// Private constructor
}
@SuppressWarnings("unchecked")
public static <T> T getInstance(Class<T> clazz) {
return (T) REGISTRY.computeIfAbsent(clazz.getName(), k -> {
try {
return clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException("Failed to create instance of " + clazz.getName(), e);
}
});
}
public static void registerInstance(String name, Object instance) {
REGISTRY.put(name, instance);
}
public static void clearRegistry() {
REGISTRY.clear();
}
}
// Usage
class ServiceA {
public void execute() {
System.out.println("ServiceA executing");
}
}
class ServiceB {
public void process() {
System.out.println("ServiceB processing");
}
}

2. Thread-Specific Singleton (ThreadLocal)

/**
* Thread-specific Singleton - separate instance per thread
*/
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
ThreadLocal.withInitial(ThreadLocalSingleton::new);
private final String threadName;
private ThreadLocalSingleton() {
this.threadName = Thread.currentThread().getName();
System.out.println("ThreadLocalSingleton created for thread: " + threadName);
}
public static ThreadLocalSingleton getInstance() {
return threadLocalInstance.get();
}
public String getThreadName() {
return threadName;
}
public void doSomething() {
System.out.println("ThreadLocalSingleton in thread: " + threadName);
}
// Clean up to prevent memory leaks
public static void remove() {
threadLocalInstance.remove();
}
}

Conclusion

The Singleton pattern is a powerful creational pattern when used appropriately. Key takeaways:

  • Use Enum Singleton for the simplest and most secure implementation
  • Consider Bill Pugh Singleton for lazy initialization without synchronization overhead
  • Use Double-Checked Locking when you need lazy initialization in multithreaded environments
  • Avoid Singleton for business logic that might need multiple instances
  • Be cautious of testing difficulties and hidden dependencies
  • Prefer Dependency Injection over Singleton in most modern applications

The pattern is most valuable for true singular resources like configuration managers, logging services, and connection pools where having multiple instances doesn't make sense and could cause issues.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper