Design Pattern: Singleton
The Singleton Pattern is a creational design pattern. Its main idea is to make sure a class can have only one instance during the whole application. This instance can be accessed globally.
Code Implementation
Key points to implement Singleton Pattern:
- Private Constructor: Prevent creating instances from outside. Only the class itself can create the single instance.
- Global Access Point: Provide a static method to let other code get this unique instance.
- Thread Safety: Make sure creating and getting the instance is safe when multiple threads run at the same time.
Let's show how to use the Singleton Pattern with a simple example: a database connection manager.
In a web application, many business methods read and write data to the database. If a new database connection is created every time, it wastes resources and adds pressure to the web app and database.
In this situation, we can use the Singleton Pattern to create a global unique database manager instance. All business code will use this instance to operate the database, avoiding repeated connection creation.
There are several ways to implement the Singleton Pattern. Different programming languages may use different methods. More details are in the comments.
1. Eager Initialization
Eager initialization creates the global unique instance when the class is loaded. This is the easiest way to implement the Singleton Pattern:
public class DatabaseManager {
// Create a globally unique instance when the class is loaded
// JVM initializes static variables when the class is loaded, ensuring thread safety
private static final DatabaseManager INSTANCE = new DatabaseManager();
private Connection connection;
// Private constructor, external code cannot create an instance
private DatabaseManager() {
// Initialize database connection
this.connection = getConnection("mysql://...", "user", "password");
System.out.println("Connection established.");
}
// Provide a public static global access point
// External code can get the unique instance through this method
public static DatabaseManager getInstance() {
return INSTANCE;
}
// Business methods
public ResultSet execute(String sql) throws SQLException {
...
}
}
class DatabaseManager {
private:
// Create a globally unique instance when the program starts
// C++ static member variables are initialized when the program starts, thread-safe
static DatabaseManager instance;
std::Connection connection;
// Private constructor, external code cannot create an instance
DatabaseManager() {
// Initialize database connection
connection = getConnection("mysql://...", "user", "password");
std::cout << "Connection established." << std::endl;
}
public:
// Delete copy constructor and assignment operator to prevent copying the singleton
DatabaseManager(const DatabaseManager&) = delete;
DatabaseManager& operator=(const DatabaseManager&) = delete;
// Provide a public static global access point
// External code can get the unique instance through this method
static DatabaseManager& getInstance() {
return instance;
}
// Business methods
std::string execute(const std::string& sql) {
// ...
}
};
// Static member definition
DatabaseManager DatabaseManager::instance;
type DatabaseManager struct {
connection *sql.DB
}
var (
// Create a globally unique instance during package initialization
instance *DatabaseManager
)
// Initialize singleton instance when package is loaded
// Go's init() function ensures thread-safe initialization
func init() {
instance = &DatabaseManager{
connection: getConnection("mysql://...", "user", "password"),
}
fmt.Println("Connection established.")
}
// Provide global access point
// External code can get the unique instance through this method
func GetInstance() *DatabaseManager {
return instance
}
// Business methods
func (dm *DatabaseManager) Execute(sql string) string {
// ...
}
class DatabaseManager {
// Create a globally unique instance when the class is loaded
// Static properties in TypeScript/JavaScript are initialized when the class is defined
private static readonly instance: DatabaseManager = new DatabaseManager();
private connection: Connection;
// Private constructor, external code cannot create an instance
private constructor() {
// Initialize database connection
this.connection = getConnection("mysql://...", "user", "password");
console.log("Connection established.");
}
// Provide a public static global access point
// External code can get the unique instance through this method
public static getInstance(): DatabaseManager {
return DatabaseManager.instance;
}
// Business methods
public execute(sql: string): string {
// ...
}
}
class DatabaseManager:
def __init__(self):
# Initialize database connection
self.connection = getConnection("mysql://...", "user", "password")
print("Connection established.")
# Business methods
def execute(self, sql: str) -> str:
# ...
# Create a globally unique instance when the module is loaded
# Python module-level variables are initialized when
# the module is first imported, naturally thread-safe
database_manager = DatabaseManager()
# Just import database_manager when using
2. Lazy Initialization (Double-Checked Locking)
Compared with eager initialization, lazy initialization means the singleton instance will be created only when it is first used.
The advantage of lazy initialization is that if the singleton is not used, no object is created. This saves memory and makes the program start faster. The downside is that extra code is needed to make sure it is thread-safe when using lazy loading.
public class DatabaseManager {
// Use volatile to ensure visibility
private static volatile DatabaseManager instance;
private Connection connection;
private DatabaseManager() {
// Initialize database connection
this.connection = getConnection("mysql://...", "user", "password");
System.out.println("Connection established.");
}
public static DatabaseManager getInstance() {
// First check, return if instance is already created
if (instance == null) {
synchronized (DatabaseManager.class) {
// Second check, only the first thread enters the
// synchronized block will create the instance
if (instance == null) {
instance = new DatabaseManager();
}
}
}
return instance;
}
// Business methods
public ResultSet execute(String sql) throws SQLException {
...
}
}
class DatabaseManager {
private:
std::Connection connection;
DatabaseManager() {
// Initialize database connection
connection = getConnection("mysql://...", "user", "password");
std::cout << "Connection established." << std::endl;
}
public:
// Delete copy constructor and assignment operator
DatabaseManager(const DatabaseManager&) = delete;
DatabaseManager& operator=(const DatabaseManager&) = delete;
static DatabaseManager* getInstance() {
// Since C++11, initialization of static local variables is thread-safe
static DatabaseManager instance;
return &instance;
}
// Business methods
std::string execute(const std::string& sql) {
// ...
}
};
type DatabaseManager struct {
connection *sql.DB
}
var (
instance *DatabaseManager
once sync.Once
)
// Use sync.Once to ensure initialization only once
// sync.Once is the best practice for implementing singleton in Go
func GetInstance() *DatabaseManager {
once.Do(func() {
instance = &DatabaseManager{
connection: getConnection("mysql://...", "user", "password"),
}
fmt.Println("Connection established.")
})
return instance
}
// Business methods
func (dm *DatabaseManager) Execute(sql string) string {
// ...
}
class DatabaseManager {
private static instance: DatabaseManager | null = null;
private connection: Connection;
private constructor() {
// Initialize database connection
this.connection = getConnection("mysql://...", "user", "password");
console.log("Connection established.");
}
public static getInstance(): DatabaseManager {
// JavaScript is single-threaded, no need to consider concurrency
if (DatabaseManager.instance === null) {
DatabaseManager.instance = new DatabaseManager();
}
return DatabaseManager.instance;
}
// Business methods
public execute(sql: string): string {
// ...
}
}
import threading
# Python decorator implementation of lazy loading singleton
def lazy_singleton(cls):
_instance = None
_lock = threading.Lock()
def get_instance(*args, **kwargs):
nonlocal _instance
if _instance is None:
with _lock:
if _instance is None:
_instance = cls(*args, **kwargs)
return _instance
return get_instance
@lazy_singleton
class DatabaseManager:
def __init__(self):
self.connection = getConnection("mysql://...", "user", "password")
print("Connection established.")
# Business methods
def execute(self, sql: str):
# ...
# --- Usage Example ---
# db1 = DatabaseManager()
# db2 = DatabaseManager()
# print(f"Are instances the same? {db1 is db2}") # Output: True
About Thread Safety
The singleton pattern only ensures that concurrent calls to the getInstance
method are thread-safe. It will create and share a single global object correctly.
Other methods (like execute
) are not guaranteed to be thread-safe by the singleton pattern. You need to ensure their safety yourself.
More Use Cases
Configuration Management
Scenario:
When the application starts, it needs to read configuration files (such as database addresses, API keys, etc.). These settings do not change during the app's lifetime and are needed everywhere.
Why use singleton:
Singleton can provide consistent configuration throughout the app and avoids reloading data, which improves performance.
// Pseudocode for usage
public class DatabaseService {
public void connect() {
// Get config from singleton
ConfigManager config = ConfigManager.getInstance();
String dbUrl = config.get("database.url");
String dbUser = config.get("database.user");
// Connect to database...
}
}
public class EmailService {
public void sendEmail() {
// Use the same config object to avoid reloading
ConfigManager config = ConfigManager.getInstance();
String apiKey = config.get("email.api.key");
String smtpHost = config.get("email.smtp.host");
// Send email...
}
}
Logger
Scenario:
In a large application, many modules need to write logs. These logs usually go to the same file or the same log server.
Why use singleton:
If every module creates its own Logger instance and writes to the same file, there will be conflicts, and the logs may be mixed up or lost. A singleton Logger ensures all logging happens through one gateway, and everything is written in order.
Popular log frameworks like Log4j and SLF4J use singleton or similar ideas to keep logs clean and centralized.
public class AppLogger {
private static final AppLogger instance = new AppLogger();
private FileWriter fileWriter;
private AppLogger() {
// All logs go to this one file
fileWriter = new FileWriter("application.log", true);
}
public static AppLogger getInstance() {
return instance;
}
public void log(String level, String message) {
...
}
}
Things to Note
Singleton pattern is powerful and useful, but be careful not to overuse it.
It brings global state, can make the code tightly coupled, and makes unit testing harder. Before you decide to use singleton, make sure the object must really be shared globally.