多例模式(Multiton):有限实例管理、实例池、线程安全实现,以及数据库连接池设计
多例模式,说白了就是单例模式的「升级版」。单例模式只允许一个实例,多例模式则允许有限个实例。你想想看,现实中有很多场景——数据库连接池、线程池、缓存池——它们都需要管理一组有限的资源,而不是只有一个。
我记得刚入行那会儿,有个同事把数据库连接池设计成了单例模式。结果呢?所有请求共用一个连接,并发一上来直接卡死。嗯,这就是典型的「用错模式」的案例。今天我们就来聊聊多例模式,看看它到底怎么用、怎么设计才靠谱。
什么是多例模式?
多例模式的核心思想很简单:限制实例的数量,并提供全局访问点。它和单例模式的区别在于——单例只维护一个实例,多例维护一组实例,通常用 Map 来管理。
- 实例数量有限,通常由枚举或配置指定
- 实例被缓存,避免重复创建
- 提供统一的获取实例的接口
- 需要处理线程安全问题
多例模式的结构
我们先画一张图,把多例模式的整体结构看清楚:
Java 实现:基础版多例模式
先来个最简单的版本。我用一个 Map 来缓存实例,用 synchronized 保证线程安全:
public class Multiton {
private static final Map<String, Multiton> instances = new HashMap<>();
private static final int MAX_INSTANCES = 3;
private final String key;
private Multiton(String key) {
this.key = key;
// 模拟初始化开销
System.out.println("创建实例: " + key);
}
public static synchronized Multiton getInstance(String key) {
// 如果实例已存在,直接返回
if (instances.containsKey(key)) {
return instances.get(key);
}
// 检查是否达到上限
if (instances.size() >= MAX_INSTANCES) {
throw new RuntimeException("实例数量已达上限: " + MAX_INSTANCES);
}
// 创建新实例
Multiton instance = new Multiton(key);
instances.put(key, instance);
return instance;
}
}
Java 实现:线程安全版(双重检查锁定)
为什么要用双重检查锁定?因为 synchronized 是重量级锁,每次调用都加锁太浪费。我们只在实例不存在时才加锁:
public class MultitonThreadSafe {
private static final Map<String, MultitonThreadSafe> instances = new ConcurrentHashMap<>();
private static final int MAX_INSTANCES = 5;
private final String key;
private MultitonThreadSafe(String key) {
this.key = key;
}
public static MultitonThreadSafe getInstance(String key) {
// 第一次检查:不加锁
MultitonThreadSafe instance = instances.get(key);
if (instance != null) {
return instance;
}
// 第二次检查:加锁后再次确认
synchronized (MultitonThreadSafe.class) {
instance = instances.get(key);
if (instance != null) {
return instance;
}
if (instances.size() >= MAX_INSTANCES) {
throw new RuntimeException("实例已达上限");
}
instance = new MultitonThreadSafe(key);
instances.put(key, instance);
return instance;
}
}
}
Java 实现:枚举版多例模式
我个人最喜欢用枚举来实现多例模式。为什么?因为枚举天然就是线程安全的,而且 JVM 保证了枚举实例的唯一性:
public enum DatabaseConnection {
PRIMARY("primary", "jdbc:mysql://primary:3306/db"),
SECONDARY("secondary", "jdbc:mysql://secondary:3306/db"),
FALLBACK("fallback", "jdbc:mysql://fallback:3306/db");
private final String name;
private final String connectionUrl;
private Connection connection;
DatabaseConnection(String name, String connectionUrl) {
this.name = name;
this.connectionUrl = connectionUrl;
// 初始化连接
this.connection = createConnection();
}
private Connection createConnection() {
// 实际项目中这里创建数据库连接
System.out.println("创建连接: " + name);
return null; // 简化处理
}
public Connection getConnection() {
return connection;
}
// 根据名称获取枚举实例
public static DatabaseConnection getByName(String name) {
for (DatabaseConnection dc : values()) {
if (dc.name.equals(name)) {
return dc;
}
}
throw new IllegalArgumentException("未知的连接名称: " + name);
}
}
- 线程安全由 JVM 保证,不需要额外同步
- 实例数量在编译期就固定了,不会出现运行时动态创建的问题
- 序列化安全,反序列化不会创建新实例
- 代码简洁,可读性强
C++ 实现:多例模式
C++ 的实现稍微复杂一些,因为我们需要手动管理内存和线程同步。这里我用 C++11 的 std::call_once 来保证线程安全:
#include <iostream>
#include <map>
#include <mutex>
#include <string>
#include <memory>
class Multiton {
private:
static std::map<std::string, std::shared_ptr<Multiton>> instances;
static std::mutex mtx;
static const int MAX_INSTANCES = 3;
std::string key;
Multiton(const std::string& key) : key(key) {
std::cout << "创建实例: " << key << std::endl;
}
public:
static std::shared_ptr<Multiton> getInstance(const std::string& key) {
// 第一次检查
auto it = instances.find(key);
if (it != instances.end()) {
return it->second;
}
// 加锁后再次检查
std::lock_guard<std::mutex> lock(mtx);
it = instances.find(key);
if (it != instances.end()) {
return it->second;
}
if (instances.size() >= MAX_INSTANCES) {
throw std::runtime_error("实例数量已达上限");
}
auto instance = std::make_shared<Multiton>(key);
instances[key] = instance;
return instance;
}
// 禁止拷贝和赋值
Multiton(const Multiton&) = delete;
Multiton& operator=(const Multiton&) = delete;
};
// 静态成员初始化
std::map<std::string, std::shared_ptr<Multiton>> Multiton::instances;
std::mutex Multiton::mtx;
实战:数据库连接池设计
好了,理论讲完了,我们来看一个实际项目中的应用——数据库连接池。这是多例模式最经典的场景。
连接池的核心需求是什么?
- 管理一组数据库连接,避免频繁创建和销毁
- 控制连接数量,防止数据库被打爆
- 线程安全,多个线程可以安全地获取和归还连接
- 支持超时和健康检查
下面是一个简化版的连接池实现,我用多例模式来管理不同类型的连接池(读库、写库、报表库):
public class ConnectionPool {
private static final Map<String, ConnectionPool> pools = new ConcurrentHashMap<>();
private final String poolName;
private final List<Connection> connections = new ArrayList<>();
private final int maxSize;
private int currentSize = 0;
private ConnectionPool(String poolName, int maxSize) {
this.poolName = poolName;
this.maxSize = maxSize;
// 初始化最小连接数
for (int i = 0; i < Math.min(2, maxSize); i++) {
connections.add(createConnection());
currentSize++;
}
}
public static ConnectionPool getPool(String poolName, int maxSize) {
return pools.computeIfAbsent(poolName, k -> new ConnectionPool(k, maxSize));
}
public synchronized Connection getConnection() throws Exception {
// 有空闲连接直接返回
for (Connection conn : connections) {
if (!isInUse(conn)) {
markInUse(conn);
return conn;
}
}
// 没有空闲连接,且未达上限,创建新连接
if (currentSize < maxSize) {
Connection conn = createConnection();
connections.add(conn);
currentSize++;
markInUse(conn);
return conn;
}
// 连接池已满,等待或抛出异常
throw new Exception("连接池已满,请稍后重试");
}
public synchronized void releaseConnection(Connection conn) {
markAvailable(conn);
}
private Connection createConnection() {
// 实际项目中创建数据库连接
return new Connection();
}
// 辅助方法,省略具体实现
private boolean isInUse(Connection conn) { return false; }
private void markInUse(Connection conn) {}
private void markAvailable(Connection conn) {}
// 内部类模拟连接
static class Connection {
private String id = java.util.UUID.randomUUID().toString();
}
}
| 要点 | 说明 |
|---|---|
| 连接复用 | 避免频繁创建/销毁,提高性能 |
| 数量控制 | 防止数据库连接数过多导致资源耗尽 |
| 线程安全 | 多个线程同时获取/归还连接时不出错 |
| 超时机制 | 获取连接超时后给出友好提示 |
| 健康检查 | 定期检查连接是否有效,无效则重建 |
多例模式 vs 单例模式 vs 对象池
很多初学者容易把多例模式和对象池搞混。我简单说一下区别:
- 单例模式: 全局只有一个实例,适合配置管理、日志等
- 多例模式: 有限个实例,每个实例有唯一标识,适合不同数据库连接、不同缓存
- 对象池: 实例数量动态变化,实例之间没有区别,适合线程池、连接池(注意:连接池其实可以看作多例模式的一种变体)
总结
多例模式是个很实用的设计模式,尤其适合管理有限资源。我个人建议:
- 如果实例数量固定且不多,优先用枚举实现
- 如果需要动态创建,用 ConcurrentHashMap + 双重检查锁定
- C++ 项目记得用智能指针,别裸奔
- 连接池设计时,别忘了健康检查和超时机制
嗯,多例模式就聊到这儿。记住一句话:有限资源,有限实例,有限管理。把握好这个度,你的系统就会既稳定又高效。
公众号:蓝海资料掘金营,微信deep3321