设计模式-单例模式

设计模式-单例模式

经验文章nimo972025-03-16 15:36:289A+A-

写在前面

Hello,我是易元,这篇文章是我学习设计模式时的笔记和心得体会。如果其中有错误,欢迎大家留言指正!

场景模拟

第一话

电商系统突现订单丢失问题,因缺乏日志追踪能力:

  • 客户投诉量单日激增300%
  • 运维团队无法定位问题根源
  • 新人工程师小易被要求48小时内实现日志系统

紧急实现方案

日志类

public class Logger {

    private PrintWriter writer;

    public Logger(String fileName) {
        try {
            writer = new PrintWriter(new FileWriter(fileName, true));
            // 加载日志模板 等等操作
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void log(String message) {
        writer.println(message);
        writer.flush();
    }

    public void close() {
        writer.close();
    }
}

测试类:

public class LoggerTest {

    @Test
    public void printTest() {
        Logger logger1 = new Logger("system.log");
        logger1.log("用户登录: ID=35876");
  logger1.close();
 
        Logger logger2 = new Logger("system.log");
        logger2.log("用户下单: 商品ID=23423");
        logger2.close();
    }

}

短期成效

  • 功能成功上线
  • 日志可追踪到70%的订单问题
  • 客户投诉量下降65%

第二话

订单问题在日志系统的帮助下迅速得以解决,但运维的投诉很快到来。

运维反馈:

  • 系统响应时间从200ms升至1200ms
  • 高峰时段出现服务不可用(错误码503)
  • JVM监控显示GC频率异常(Young GC 5次/秒)

问题根因分析

  1. 每次调用创建Logger --> 频繁文件IO操作 --> 磁盘写入队列阻塞
  2. 大量对象创建/回收 --> GC压力剧增

以上两点同时导致系统整体性能下降

单例模式拯救计划

重构目标

  1. 消除资源浪费
  • 单实例共享日志文件句柄
  1. 统一写入控制
  • 避免多线程写入竞争
  1. 延长对象生命周期
  • 减少GC压力

重构思路

  1. 私有构造函数
  2. 防止外部直接创建 Logger 实例。
  3. 静态实例持有
  4. 通过静态方法返回唯一的 Logger 实例,保证全局唯一性。
  5. 全局访问点
  6. 提供统一访问入口,在第一次调用时创建实例(synchronized + 双重检查锁) ,避免资源浪费。

重构后代码

日志类

public class Logger {
    private static volatile Logger instance;
    private final File logFile;
    private final FileWriter writer;
    
    private Logger() throws IOException {
        this.logFile = new File("system.log");
        this.writer = new FileWriter(logFile, true);
        Runtime.getRuntime().addShutdownHook(new Thread(this::close));
    }
    
    public static Logger getInstance() {
        if (instance == null) {
            synchronized (Logger.class) {
                if (instance == null) {
                    try {
                        instance = new Logger();
                    } catch (IOException e) {
                        throw new RuntimeException("日志初始化失败", e);
                    }
                }
            }
        }
        return instance;
    }
    
    public synchronized void log(String message) {
        try {
            writer.write(LocalDateTime.now() + " - " + message + "\n");
            writer.flush();
        } catch (IOException e) {
            System.err.println("日志写入异常: " + e.getMessage());
        }
    }
    
    private void close() {
        try {
            writer.close();
        } catch (IOException e) {
            System.err.println("日志关闭失败: " + e.getMessage());
        }
    }
}

测试类:

    @Test
    public void printTest() {
        Logger logger01 = Logger.getInstance();
        logger01.log("用户登录: ID=35876");

        Logger logger02 = Logger.getInstance();
        logger01.log("用户下单: 商品ID=23423");

        System.out.println(logger01.equals(logger02)); // true
    }

重构效果对比

重构前

  • 日志写入耗时 -> 120ms ~ 350ms
  • Full GC频率 -> 2次/小时
  • 文件描述符占用 -> 峰值800+

重构后

  • 日志写入耗时 -> 15ms ~ 40ms
  • Full GC频率 -> 0.2次/小时
  • 文件描述符占用 -> 恒定1个

平稳运行成果

  • 系统平均响应时间恢复至210ms
  • 高峰时段吞吐量提升3倍
  • 日志相关故障归零

开源项目中的应用

Apache HttpComponents Client 项目中单例模式的应用

PoolingHttpClientConnectionManager

public class PoolingHttpClientConnectionManager extends AbstractConnPool implements Closeable {

    // 定义并初始化一个静态常量 DEFAULT_INSTANCE,使用final关键字确保创建后无法变更
 // 它是整个 JVM 生命周期内唯一的 PoolingHttpClientConnectionManager 实例
    private static final PoolingHttpClientConnectionManager DEFAULT_INSTANCE = createDefaultInstance();

    // 创建默认 PoolingHttpClientConnectionManager 实例
    private static PoolingHttpClientConnectionManager createDefaultInstance() {
        try {
            SSLSocketFactory sslsf = SSLContext.getDefault().getSocketFactory();
            HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
            Registry registry = RegistryBuilder.create()
                    .register("http", PlainConnectionSocketFactory.getSocketFactory())
                    .register("https", new SSLConnectionSocketFactory(sslsf, hostnameVerifier))
                    .build();
            
            return new PoolingHttpClientConnectionManager(registry);
        } catch (NoSuchAlgorithmException | KeyManagementException ex) {
            throw new RuntimeException(ex.getMessage(), ex);
        }
    }


   // 获取预定义好配置参数的 `PoolingHttpClientConnectionManager` 实例
    public static PoolingHttpClientConnectionManager getDefault() {
        return DEFAULT_INSTANCE;
    }

    // Other methods related to managing connections...
    protected PoolingHttpClientConnectionManager(final SchemeRegistry schemeRegistry,
                                               final DnsResolver dnsResolver) {
        super(schemeRegistry, dnsResolver);
    }

    protected PoolingHttpClientConnectionManager(
            final Registry socketFactoryRegistry,
            final DNSLookupService dnsResolver) {
        super(socketFactoryRegistry, dnsResolver);
    }

    // Additional constructors and other utility methods...
    @Override
    public void close() throws IOException {
        shutdown(); // Properly shuts down all managed connections in the pool.
    }
}
  • 全局唯一性: 只有一个 PoolingHttpClientConnectionManager 默认实例,在整个应用程序生命周期内有效,减少了重复创建工作带来的额外消耗。
  • 集中控制: 所有基于该连接管理器建立的 HTTP/HTTPS 请求都会遵循一致的行为准则和服务质量标准。

长话短说

单例模式的核心内容

核心思想

  • 确保一个类只有一个实例,并提供一个全局访问点。

使用思路

  1. 私有化构造函数
  2. 将类的构造函数设置为私有,所属类静态方法仍能调用构造函数,但其他外部类无法直接创建。
  3. 私有静态成员变量
  4. 在类中添加一个私有成员变量用于保存单例实例。
  5. 提供静态方法
  6. 声明一个公有静态方法用于获取单例实例,若实例为空则创建一个新的对象并存储在静态成员变量中,并返回实例,若不为空则返回实例。

何时可以使用单例模式?

  1. 全局唯一对象
  2. 当需要一个全局唯一的对象时(如配置管理器、日志系统)。
  3. 资源节省
  4. 当对象的创建成本高,且需要频繁使用时。
  5. 统一管理
  6. 当需要统一管理和配置某个对象时。

单例模式实现方式较多分别有:饿汉式(加载即创建实例)、懒汉式(延迟实例化)、双重锁(通过懒汉式+锁,保证线程安全)、静态内部类、枚举等。

点击这里复制本文地址 以上内容由nimo97整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!
qrcode

尼墨宝库 © All Rights Reserved.  蜀ICP备2024111239号-7