多例模式(Multiton):有限实例管理、实例池、线程安全实现,以及数据库连接池设计

多例模式,说白了就是单例模式的「升级版」。单例模式只允许一个实例,多例模式则允许有限个实例。你想想看,现实中有很多场景——数据库连接池、线程池、缓存池——它们都需要管理一组有限的资源,而不是只有一个。

我记得刚入行那会儿,有个同事把数据库连接池设计成了单例模式。结果呢?所有请求共用一个连接,并发一上来直接卡死。嗯,这就是典型的「用错模式」的案例。今天我们就来聊聊多例模式,看看它到底怎么用、怎么设计才靠谱。

什么是多例模式?

多例模式的核心思想很简单:限制实例的数量,并提供全局访问点。它和单例模式的区别在于——单例只维护一个实例,多例维护一组实例,通常用 Map 来管理。

核心要点:
  • 实例数量有限,通常由枚举或配置指定
  • 实例被缓存,避免重复创建
  • 提供统一的获取实例的接口
  • 需要处理线程安全问题

多例模式的结构

我们先画一张图,把多例模式的整体结构看清楚:

多例模式结构图 Client(客户端) Multiton(多例管理器) - instances: Map<String, Instance> Instance 1 key="primary" Instance 2 key="secondary" Instance 3 key="fallback" 实例数量固定,通过 key 来区分和获取 ⚠ 线程安全:双重检查锁定 / 枚举 典型应用:数据库连接池

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;
    }
}
个人经验: 我早期写多例模式时,直接在 getInstance 方法上加 synchronized。这在实例少、并发低的时候没问题。但一旦实例多了,性能瓶颈就出来了。后面我会讲怎么优化。

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 5 之前有内存可见性问题,需要 volatile 关键字。但在 ConcurrentHashMap 的帮助下,我们其实可以更优雅地解决——直接用 putIfAbsent 方法。

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;
避坑指南: 我曾经在 C++ 项目里直接用 raw pointer 管理多例实例,结果内存泄漏查了三天。后来改用 shared_ptr,问题迎刃而解。记住:C++ 里能用智能指针就别用裸指针。

实战:数据库连接池设计

好了,理论讲完了,我们来看一个实际项目中的应用——数据库连接池。这是多例模式最经典的场景。

连接池的核心需求是什么?

  • 管理一组数据库连接,避免频繁创建和销毁
  • 控制连接数量,防止数据库被打爆
  • 线程安全,多个线程可以安全地获取和归还连接
  • 支持超时和健康检查

下面是一个简化版的连接池实现,我用多例模式来管理不同类型的连接池(读库、写库、报表库):

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