设计模式:单例模式
单例模式(Singleton Pattern)是一种创建型设计模式,其核心思想是保证一个类在整个应用程序生命周期内只能创建一个实例,并提供全局访问点来获取这个唯一的实例。
代码实现
实现单例模式的几个关键点:
- 私有构造函数:防止外部代码创建实例,仅允许类内部实例化唯一的一个实例。
- 全局访问点:提供一个静态方法,允许外部代码获取这个唯一的实例。
- 并发安全:确保在多线程环境下,创建实例和获取实例都是线程安全的。
让我们通过一个简化的数据库连接管理器的例子来演示单例模式的实现以及应用场景。
在一个 Web 应用中,肯定有很多业务方法都会向数据库读写数据。如果每次操作数据库都创建新的连接,会造成资源浪费,对 Web 应用和数据库造成压力。
这种情况下,我们可以用单例模式实现一个全局唯一的数据库管理实例,确保所有业务代码都通过这个实例来操作数据库,从而避免重复创建数据库连接。
单例模式有几种实现方式,下面一一介绍。不同编程语言的实现方式略有不同,详细的讲解放在注释中。
1. 饿汉式实现
饿汉式在类初始化的时候就创建全局唯一实例,是实现单例模式最简单的方式:
public class DatabaseManager {
// 在类加载时创建全局唯一的实例
// JVM 在类加载时会初始化静态变量,确保线程安全
private static final DatabaseManager INSTANCE = new DatabaseManager();
private Connection connection;
// 私有构造函数,外部代码无法创建实例
private DatabaseManager() {
// 初始化数据库连接
this.connection = getConnection("mysql://...", "user", "password");
System.out.println("Connection established.");
}
// 提供 public static 的全局访问点
// 外部代码通过这个方法获取这个唯一的实例
public static DatabaseManager getInstance() {
return INSTANCE;
}
// 业务方法
public ResultSet execute(String sql) throws SQLException {
...
}
}
class DatabaseManager {
private:
// 在程序启动时创建全局唯一的实例
// C++ 静态成员变量在程序启动时初始化,线程安全
static DatabaseManager instance;
std::Connection connection;
// 私有构造函数,外部代码无法创建实例
DatabaseManager() {
// 初始化数据库连接
connection = getConnection("mysql://...", "user", "password");
std::cout << "Connection established." << std::endl;
}
public:
// 删除拷贝构造函数和赋值操作符,防止复制单例
DatabaseManager(const DatabaseManager&) = delete;
DatabaseManager& operator=(const DatabaseManager&) = delete;
// 提供 public static 的全局访问点
// 外部代码通过这个方法获取这个唯一的实例
static DatabaseManager& getInstance() {
return instance;
}
// 业务方法
std::string execute(const std::string& sql) {
// ...
}
};
// 静态成员定义
DatabaseManager DatabaseManager::instance;
type DatabaseManager struct {
connection *sql.DB
}
var (
// 在包初始化时创建全局唯一的实例
instance *DatabaseManager
)
// 在包加载时初始化单例实例
// Go 的 init() 函数保证线程安全的初始化
func init() {
instance = &DatabaseManager{
connection: getConnection("mysql://...", "user", "password"),
}
fmt.Println("Connection established.")
}
// 提供全局访问点
// 外部代码通过这个方法获取这个唯一的实例
func GetInstance() *DatabaseManager {
return instance
}
// 业务方法
func (dm *DatabaseManager) Execute(sql string) string {
// ...
}
class DatabaseManager {
// 在类加载时创建全局唯一的实例
// TypeScript/JavaScript 中静态属性在类定义时初始化
private static readonly instance: DatabaseManager = new DatabaseManager();
private connection: Connection;
// 私有构造函数,外部代码无法创建实例
private constructor() {
// 初始化数据库连接
this.connection = getConnection("mysql://...", "user", "password");
console.log("Connection established.");
}
// 提供 public static 的全局访问点
// 外部代码通过这个方法获取这个唯一的实例
public static getInstance(): DatabaseManager {
return DatabaseManager.instance;
}
// 业务方法
public execute(sql: string): string {
// ...
}
}
class DatabaseManager:
def __init__(self):
# 初始化数据库连接
self.connection = getConnection("mysql://...", "user", "password")
print("Connection established.")
# 业务方法
def execute(self, sql: str) -> str:
# ...
# 在模块加载时创建全局唯一的实例
# Python 模块级别的变量在模块首次导入时初始化,天然线程安全
database_manager = DatabaseManager()
# 使用时直接 import database_manager 即可
2. 懒汉式实现(双重检查锁定)
懒汉式相比饿汉式,支持懒加载,即在第一次使用时才创建单例实例。
懒加载的好处是,单例未被使用时就不会创建实例,可以节约内存并提升程序启动速度;坏处是需要额外添加代码确保懒加载的线程安全。
public class DatabaseManager {
// 使用 volatile 保证可见性
private static volatile DatabaseManager instance;
private Connection connection;
private DatabaseManager() {
// 初始化数据库连接
this.connection = getConnection("mysql://...", "user", "password");
System.out.println("Connection established.");
}
public static DatabaseManager getInstance() {
// 第一次检查,已创建实例则直接返回
if (instance == null) {
synchronized (DatabaseManager.class) {
// 第二次检查,第一次进入同步块的线程才会创建实例
if (instance == null) {
instance = new DatabaseManager();
}
}
}
return instance;
}
// 业务方法
public ResultSet execute(String sql) throws SQLException {
...
}
}
class DatabaseManager {
private:
std::Connection connection;
DatabaseManager() {
// 初始化数据库连接
connection = getConnection("mysql://...", "user", "password");
std::cout << "Connection established." << std::endl;
}
public:
// 删除拷贝构造函数和赋值操作符
DatabaseManager(const DatabaseManager&) = delete;
DatabaseManager& operator=(const DatabaseManager&) = delete;
static DatabaseManager* getInstance() {
// C++11 起,静态局部变量的初始化是线程安全的
static DatabaseManager instance;
return &instance;
}
// 业务方法
std::string execute(const std::string& sql) {
// ...
}
};
type DatabaseManager struct {
connection *sql.DB
}
var (
instance *DatabaseManager
once sync.Once
)
// 使用 sync.Once 确保只初始化一次
// sync.Once 是 Go 中实现单例的最佳实践
func GetInstance() *DatabaseManager {
once.Do(func() {
instance = &DatabaseManager{
connection: getConnection("mysql://...", "user", "password"),
}
fmt.Println("Connection established.")
})
return instance
}
// 业务方法
func (dm *DatabaseManager) Execute(sql string) string {
// ...
}
class DatabaseManager {
private static instance: DatabaseManager | null = null;
private connection: Connection;
private constructor() {
// 初始化数据库连接
this.connection = getConnection("mysql://...", "user", "password");
console.log("Connection established.");
}
public static getInstance(): DatabaseManager {
// JavaScript 是单线程的,不需要考虑并发问题
if (DatabaseManager.instance === null) {
DatabaseManager.instance = new DatabaseManager();
}
return DatabaseManager.instance;
}
// 业务方法
public execute(sql: string): string {
// ...
}
}
import threading
# Python 装饰器实现懒加载单例
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.")
# 业务方法
def execute(self, sql: str):
# ...
# --- 使用示例 ---
# db1 = DatabaseManager()
# db2 = DatabaseManager()
# print(f"Are instances the same? {db1 is db2}") # Output: True
关于线程安全
单例模式仅确保并发调用 getInstance
方法是线程安全的,能够正确地创建并共享一个全局单例对象。
其他方法(比如 execute
)的线程安全不在单例模式的考虑范围内,需要自行实现。
更多使用场景
配置管理场景
场景描述:
应用启动时需要读取配置文件(如数据库地址、API密钥等),这些配置在整个应用生命周期内不变,且需要全局访问。
为什么使用单例:
使用单例模式可以提供全局一致的配置信息,同时避免重复加载数据提升性能。
// 使用场景伪代码
public class DatabaseService {
public void connect() {
// 从单例获取配置
ConfigManager config = ConfigManager.getInstance();
String dbUrl = config.get("database.url");
String dbUser = config.get("database.user");
// 建立数据库连接...
}
}
public class EmailService {
public void sendEmail() {
// 同一个配置实例,避免重复加载配置
ConfigManager config = ConfigManager.getInstance();
String apiKey = config.get("email.api.key");
String smtpHost = config.get("email.smtp.host");
// 发送邮件...
}
}
日志记录器
场景描述:
在一个大型应用中,各个模块都需要记录日志。这些日志信息通常需要被写入到同一个文件中,或者发送到同一个日志服务器。
为什么使用单例:
如果每个模块都创建自己的 Logger 实例来写同一个日志文件,就会发生文件写入冲突,导致日志内容混乱或丢失。一个单例的 Logger 对象可以确保所有的日志操作都通过一个入口点,有序地写入到指定的目标。
像 Log4j, SLF4J 这类成熟的日志框架,都运用了单例或类似的思想来确保日志配置的统一和输出的协调。
public class AppLogger {
private static final AppLogger instance = new AppLogger();
private FileWriter fileWriter;
private AppLogger() {
// 所有日志都写入到这一个文件中
fileWriter = new FileWriter("application.log", true);
}
public static AppLogger getInstance() {
return instance;
}
public void log(String level, String message) {
...
}
}
注意事项
单例模式虽然强大且应用广泛,但也需要警惕滥用。
因为它会引入全局状态,增加代码的耦合度,给单元测试带来困难。因此,在决定使用单例模式前,请务必确认该对象确实需要全局共享。