单例模式:设计与实践

37 阅读8分钟

单例模式:设计与实践

一、什么是单例模式

1. 基本定义

单例模式(Singleton Pattern)是一种创建型设计模式,由《设计模式:可复用面向对象软件的基础》(即“四人帮”GOF著作)定义为:保证一个类仅有一个实例,并提供一个访问它的全局访问点。这一模式通过控制类的实例化过程,确保在系统中该类的实例唯一存在,从而实现对共享资源的集中管理。

2. 核心思想

通过私有化构造方法阻止外部直接创建实例,同时在类内部自行创建唯一实例,并提供静态方法作为全局访问点。这种设计使得系统中所有模块都能共享该实例,避免了因多实例存在导致的资源竞争、状态不一致等问题。

二、单例模式的特点

1. 唯一性

类在整个应用生命周期内仅存在一个实例,任何时候通过访问点获取的实例都指向同一个内存地址。

2. 全局访问性

通过静态方法提供全局统一的访问入口,无需依赖对象引用传递即可在系统任意模块中获取实例。

3. 私有构造方法

构造方法被声明为private,禁止外部通过new关键字创建实例,从根源上控制实例数量。

4. 延迟初始化(可选)

部分实现中支持延迟初始化(如懒汉式),即实例在首次被使用时才创建,而非类加载时,可减少初始化阶段的资源消耗。

特点说明
唯一性类在应用生命周期内仅存在一个实例
全局访问性提供静态方法作为全局统一访问入口
私有构造方法禁止外部通过new创建实例
延迟初始化(可选)按需创建实例,减少初始资源占用

三、单例模式的标准代码实现

1. 饿汉式实现

饿汉式在类加载时即初始化实例,基于类加载机制保证线程安全。

public class SingletonHungry {
    // 类加载时初始化单例实例
    private static final SingletonHungry INSTANCE = new SingletonHungry();

    // 私有构造方法
    private SingletonHungry() {
        // 防止反射破坏单例(可选)
        if (INSTANCE != null) {
            throw new IllegalStateException("单例实例已存在");
        }
    }

    // 全局访问点
    public static SingletonHungry getInstance() {
        return INSTANCE;
    }
}

特点

  • 线程安全:类加载过程由JVM保证线程安全,避免多线程竞争问题
  • 初始化时机:类加载时即创建实例,可能提前占用系统资源
  • 实现简单:无需额外同步机制,代码简洁

2. 懒汉式实现(线程安全)

懒汉式在首次调用getInstance()时创建实例,通过同步机制保证线程安全。

public class SingletonLazy {
    // 用volatile修饰防止指令重排序
    private static volatile SingletonLazy instance;

    // 私有构造方法
    private SingletonLazy() {
        // 防止反射破坏单例(可选)
        if (instance != null) {
            throw new IllegalStateException("单例实例已存在");
        }
    }

    // 双重检查锁定实现线程安全的延迟初始化
    public static SingletonLazy getInstance() {
        // 第一次检查:避免不必要的同步
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                // 第二次检查:确保同步块内实例仍未被创建
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}

特点

  • 延迟初始化:实例在首次使用时创建,节省初始资源
  • 线程安全:通过synchronizedvolatile保证多线程环境下的正确性
  • 性能优化:双重检查锁定减少同步开销,适合高并发场景

四、支付框架设计中的模式运用

1. 应用场景分析

支付配置管理器负责管理商户信息、接口密钥、支付渠道参数等核心配置,这些配置在签名验证、渠道路由、回调处理等环节被频繁使用。若存在多实例,可能导致配置不一致(如不同实例加载的密钥不同),引发支付失败、数据篡改风险。单例模式可确保配置全局一致,同时避免重复加载配置文件带来的IO开销。

2. 饿汉式实现

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class PaymentConfigManager {
    // 类加载时初始化单例实例
    private static final PaymentConfigManager INSTANCE = new PaymentConfigManager();

    // 配置存储容器
    private final Properties configProps;
    // 读写锁:优化多线程下的配置更新与读取
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    // 私有构造方法,加载配置
    private PaymentConfigManager() {
        // 防止反射破坏单例
        if (INSTANCE != null) {
            throw new IllegalStateException("PaymentConfigManager实例已存在");
        }
        configProps = new Properties();
        loadConfigFromFile();
    }

    // 从配置文件加载配置
    private void loadConfigFromFile() {
        try (InputStream is = getClass().getResourceAsStream("/payment-config.properties")) {
            if (is == null) {
                throw new RuntimeException("支付配置文件/payment-config.properties不存在");
            }
            configProps.load(is);
        } catch (IOException e) {
            throw new RuntimeException("加载支付配置失败", e);
        }
    }

    // 全局访问点
    public static PaymentConfigManager getInstance() {
        return INSTANCE;
    }

    // 获取商户ID
    public String getMerchantId() {
        rwLock.readLock().lock();
        try {
            return configProps.getProperty("merchant.id");
        } finally {
            rwLock.readLock().unlock();
        }
    }

    // 获取支付宝API密钥
    public String getAlipayApiKey() {
        rwLock.readLock().lock();
        try {
            return configProps.getProperty("alipay.api.key");
        } finally {
            rwLock.readLock().unlock();
        }
    }

    // 获取微信支付商户证书路径
    public String getWechatPayCertPath() {
        rwLock.readLock().lock();
        try {
            return configProps.getProperty("wechatpay.cert.path");
        } finally {
            rwLock.readLock().unlock();
        }
    }

    // 获取支付回调地址
    public String getPaymentCallbackUrl() {
        rwLock.readLock().lock();
        try {
            return configProps.getProperty("payment.callback.url");
        } finally {
            rwLock.readLock().unlock();
        }
    }

    // 动态更新配置(支持配置中心推送更新)
    public void updateConfig(String key, String value) {
        rwLock.writeLock().lock();
        try {
            configProps.setProperty(key, value);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    // 通用配置获取方法
    public String getConfig(String key) {
        rwLock.readLock().lock();
        try {
            return configProps.getProperty(key);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}
2.1 配置文件内容
# 商户基础信息
merchant.id=MCH20230001
merchant.name=XX电商平台
merchant.notify.url=https://api.xxx.com/merchant/notify

# 支付宝配置
alipay.api.key=MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAO5
alipay.app.id=2021000000000001
alipay.gateway=https://openapi.alipay.com/gateway.do
alipay.sign.type=RSA2

# 微信支付配置
wechatpay.api.key=333666888abcdef
wechatpay.mch.id=1600000000
wechatpay.cert.path=/certs/wechatpay_cert.p12
wechatpay.gateway=https://api.mch.weixin.qq.com/v3/pay/

# 通用配置
payment.callback.url=https://api.xxx.com/payment/callback
payment.timeout.seconds=300
payment.max.amount.per.transaction=100000.00
2.2 实际使用示例
// 支付签名工具类
public class PaymentSignUtil {
    // 生成支付宝签名
    public static String generateAlipaySign(String content) {
        // 获取配置管理器实例
        PaymentConfigManager configManager = PaymentConfigManager.getInstance();
        // 获取支付宝密钥(实际场景需结合加密算法)
        String apiKey = configManager.getAlipayApiKey();
        String signType = configManager.getConfig("alipay.sign.type");

        // 执行签名逻辑
        return AlipaySignature.rsa256Sign(content, apiKey, "UTF-8");
    }
}

// 支付渠道路由服务
public class PaymentChannelRouter {
    public String routeChannel(PaymentRequest request) {
        PaymentConfigManager configManager = PaymentConfigManager.getInstance();
        // 获取单笔最大金额配置用于渠道路由
        String maxAmount = configManager.getConfig("payment.max.amount.per.transaction");

        // 根据金额选择渠道(示例逻辑)
        if (request.getAmount().compareTo(new BigDecimal(maxAmount)) > 0) {
            return "unionpay"; // 大额走银联
        } else {
            return "alipay"; // 小额走支付宝
        }
    }
}

3. 实现要点说明

  • 线程安全增强:通过ReentrantReadWriteLock区分读写操作,允许多个线程同时读取配置,仅在更新时加写锁,提升并发性能
  • 防反射破坏:在构造方法中检查实例是否已存在,防止通过反射强制创建新实例
  • 配置扩展性:提供getConfig(String key)通用方法支持动态配置项,无需频繁修改类结构
  • 资源管理:采用try-with-resources确保配置文件流正确关闭,避免资源泄露

五、开源框架设计中的模式运用

1. 核心单例组件:SqlSessionFactory

SqlSessionFactory是MyBatis的核心工厂类,负责创建SqlSession实例,在应用中通常以单例形式存在。

2. 实现机制

2.1 实例创建过程
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;

public class MyBatisConfig {
    // 单例SqlSessionFactory实例
    private static final SqlSessionFactory sqlSessionFactory;

    // 静态初始化块:类加载时创建实例
    static {
        try {
            // 加载MyBatis核心配置文件
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);

            // 构建SqlSessionFactory(全局唯一)
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            throw new ExceptionInInitializerError("初始化SqlSessionFactory失败: " + e.getMessage());
        }
    }

    // 全局访问方法
    public static SqlSessionFactory getSqlSessionFactory() {
        return sqlSessionFactory;
    }
}
2.2 单例必要性分析
  • 资源密集型创建SqlSessionFactory创建过程需解析XML配置、初始化数据源、构建映射关系等,耗时较长,单例模式可避免重复开销
  • 连接池管理SqlSessionFactory内部持有DataSource(如PooledDataSource),单例确保连接池唯一,避免连接资源碎片化
  • 配置一致性:包含全局配置的Configuration对象在SqlSessionFactory中唯一存在,保证所有SqlSession使用统一的映射规则和插件配置
2.3 框架级单例保障

MyBatis并未在SqlSessionFactory内部强制单例,而是通过框架设计引导开发者使用单例模式:

  • 提供SqlSessionFactoryBuilder作为构建器,明确区分“创建”与“使用”职责
  • SqlSessionFactory接口无public构造方法,只能通过SqlSessionFactoryBuilder创建
  • 官方文档明确建议:“SqlSessionFactory一旦创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例”

六、总结

1. 单例模式的适用场景

  • 资源管理器:如配置中心、连接池、线程池等需要集中管理的组件
  • 工具类:日志工具、加密工具等无状态或共享状态的工具类
  • 框架核心组件:如MyBatis的SqlSessionFactory等全局唯一的核心对象

2. 实现选择原则

  • 饿汉式:适合初始化成本低、必须在启动时加载的场景,优势是线程安全简单可靠
  • 懒汉式:适合初始化成本高、可能不被使用的场景,需注意双重检查锁定的正确实现

3. 风险与规避

  • 线程安全风险:多线程环境下需确保实例创建过程的原子性,避免半初始化实例
  • 序列化风险:实现Serializable接口的单例需重写readResolve()方法防止反序列化创建新实例
  • 测试难度增加:单例模式可能导致单元测试依赖全局状态,可通过依赖注入接口化单例解决

4. 支付系统中的实践价值

在支付领域,单例模式通过保障配置一致性、资源集中管理和状态统一性,直接提升了系统的安全性(如密钥唯一)、稳定性(如连接池管理)和性能(如减少重复初始化),是支付核心组件设计的重要模式选择。