Java设计模式-单例模式

发布于:2025-08-16 ⋅ 阅读:(16) ⋅ 点赞:(0)

Java设计模式-单例模式

模式概述

单例模式简介

核心思想:确保一个类在整个应用程序生命周期中仅创建一个实例,并提供一个全局访问点来获取该实例。通过私有化构造方法和控制实例化逻辑,单例模式避免了重复创建对象的开销,确保了全局状态的一致性。

模式类型:创建型模式(Creational Pattern)。

作用

  • 唯一实例控制:确保类仅存在一个实例,避免因多次实例化导致的资源浪费(如数据库连接池、配置管理器);
  • 全局访问点:提供统一的全局访问入口,简化对象访问逻辑(如日志系统只需一个实例记录日志);
  • 节省资源:对于高开销或需共享状态的对象(如线程池、缓存管理器),单例模式避免了重复初始化的资源消耗;
  • 状态一致性:全局唯一实例确保所有客户端访问的是同一对象状态,避免因状态不一致导致的逻辑错误。

典型应用场景

  • 全局配置管理:如读取应用配置(config.properties)的ConfigManager,只需一个实例加载配置;
  • 日志系统Logger类需全局共享,避免多个实例重复写入日志文件;
  • 数据库连接池ConnectionPool需统一管理连接,单例模式确保所有请求共享同一连接池;
  • 缓存管理器CacheManager存储高频访问数据,单例模式避免重复创建缓存实例;
  • 设备驱动程序:如打印机驱动(PrinterDriver),系统中仅需一个实例控制硬件。

我认为:单例模式就像应用中的“唯一管家”,负责管理一个关键资源,确保所有需要它的地方都通过同一个入口获取,既避免了资源浪费,又保证了操作的一致性。

课程目标

  • 理解单例模式的核心思想和经典应用场景
  • 识别应用场景,使用单例模式解决功能要求
  • 了解单例模式的优缺点

核心组件

角色-职责表

角色 职责 示例类名
单例类(Singleton) 私有化构造方法,控制实例创建逻辑,提供全局访问点获取唯一实例。 ConfigManager(配置管理器)、Logger(日志记录器)

类图

下面是一个简化的类图表示,展示了单例模式中的主要角色及其交互方式:

调用 getInstance()
Singleton
-static Singleton instance # 静态私有实例
-Singleton()
+static Singleton getInstance()
Client

传统实现 VS 单例模式

案例需求

案例背景:设计一个日志记录器(Logger),要求全局仅存在一个实例,所有模块通过该实例记录日志(如写入文件或控制台)。传统方式通过new Logger()创建实例,导致多个日志实例重复写入文件,引发数据混乱。

传统实现(痛点版)

代码实现

// 传统方式:无单例控制,可随意创建多个实例
public class Logger {
    private String logFile; // 日志文件路径

    // 公开构造方法(问题根源:允许外部任意创建实例)
    public Logger(String logFile) {
        this.logFile = logFile;
        System.out.println("初始化日志文件:" + logFile);
    }

    // 日志记录方法
    public void log(String message) {
        System.out.println("写入日志[" + logFile + "]:" + message);
    }
}

// 客户端使用(错误示范)
public class Client {
    public static void main(String[] args) {
        // 创建第一个日志实例(写入app.log)
        Logger logger1 = new Logger("app.log");
        logger1.log("用户登录"); 

        // 创建第二个日志实例(重复初始化,写入error.log)
        Logger logger2 = new Logger("error.log");
        logger2.log("系统异常"); 
    }
}

痛点总结

  • 实例泛滥:外部可通过new无限制创建实例,导致多个日志实例重复初始化(如重复打开文件),浪费资源;
  • 状态不一致:不同实例指向不同日志文件(如app.logerror.log),客户端无法保证全局日志统一;
  • 数据混乱:多个实例同时写入不同文件,无法追踪完整的日志流(如用户登录和系统异常需关联分析)。

单例模式 实现(优雅版)

代码实现

// 单例模式:Logger(线程安全、懒加载)
public class Logger {
    // 1. 静态私有实例(volatile保证可见性和禁止指令重排)
    private static volatile Logger instance;
    
    // 2. 私有构造方法(禁止外部new)
    private Logger(String logFile) {
        System.out.println("初始化日志文件:" + logFile);
        // 实际场景:加载配置、打开文件流等高开销操作(仅执行一次)
    }

    // 3. 全局访问方法(双重检查锁定保证线程安全)
    public static Logger getInstance(String logFile) {
        // 第一次检查:避免不必要的同步
        if (instance == null) {
            synchronized (Logger.class) { // 同步类对象
                // 第二次检查:防止多线程同时通过第一次检查后重复创建
                if (instance == null) {
                    instance = new Logger(logFile); 
                }
            }
        }
        return instance;
    }

    // 日志记录方法
    public void log(String message) {
        System.out.println("写入日志[" + logFile + "]:" + message); 
        // 实际场景:写入文件或发送到日志服务
    }
}

// 客户端使用(正确示范)
public class Client {
    public static void main(String[] args) {
        // 获取单例实例(仅初始化一次)
        Logger logger1 = Logger.getInstance("app.log");
        logger1.log("用户登录"); 

        // 再次获取实例(返回已存在的实例)
        Logger logger2 = Logger.getInstance("error.log"); // 注意:此处logFile会被忽略!
        logger2.log("系统异常"); 
    }
}
  • 优化说明

    上述代码存在一个问题:第二次调用getInstance("error.log")时,由于instance已存在,会直接返回第一次创建的app.log实例,导致error.log被忽略。因此,正确的单例模式需在首次调用时固定日志文件路径。以下是修正后的实现:

    public class Logger {
        private static volatile Logger instance;
        private final String logFile; // 关键:设为final,确保初始化后不可修改
    
        // 私有构造方法(参数在首次调用时传入)
        private Logger(String logFile) {
            this.logFile = logFile; 
            System.out.println("初始化日志文件:" + logFile);
        }
    
        // 全局访问方法(首次调用时指定logFile,后续调用忽略参数)
        public static Logger getInstance(String logFile) {
            if (instance == null) {
                synchronized (Logger.class) {
                    if (instance == null) {
                        instance = new Logger(logFile); // 仅首次调用时使用参数
                    }
                }
            }
            return instance;
        }
    
        public void log(String message) {
            System.out.println("写入日志[" + logFile + "]:" + message); 
        }
    }
    
    // 客户端正确使用
    public class Client {
        public static void main(String[] args) {
            // 首次调用指定日志文件(必须!否则instance为null时参数被忽略)
            Logger logger1 = Logger.getInstance("app.log");
            logger1.log("用户登录"); 
    
            // 后续调用无需关心日志文件(已被首次调用固定)
            Logger logger2 = Logger.getInstance("error.log"); 
            logger2.log("系统异常"); // 实际写入app.log(因为instance已初始化)
        }
    }
    

    优势

    • 唯一实例:通过私有构造和双重检查锁定,确保全局仅一个实例;
    • 懒加载:实例在首次调用getInstance()时创建,避免类加载时立即初始化(节省资源);
    • 线程安全synchronized同步类对象,防止多线程并发创建实例;
    • 高内聚:日志文件路径在首次初始化时固定,后续操作无需关心路径,避免状态混乱。

    局限

    • 序列化问题:若单例类实现Serializable接口,反序列化时会创建新实例(破坏单例),需重写readResolve()方法返回现有实例;
    • 反射攻击:通过反射调用私有构造方法(setAccessible(true))可创建新实例,需通过异常处理或标记私有构造方法防御;
    • 多类加载器问题:不同类加载器加载同一单例类时,会创建多个实例(需限定类加载器范围)。

模式变体

变体 实现方式 特点 适用场景
饿汉式 类加载时直接初始化实例(private static final Singleton instance = new Singleton(); 简单、线程安全,但可能浪费资源(若实例未被使用) 实例创建成本低、确定会被使用的场景
懒汉式(非线程安全) 首次调用时创建实例(if (instance == null) instance = new Singleton(); 简单但线程不安全(多线程可能创建多个实例) 单线程环境或测试场景
双重检查锁定 结合volatilesynchronized,首次调用时加锁创建实例 线程安全、懒加载、高效(仅首次同步) 生产环境主流实现
静态内部类 利用类加载机制,通过静态内部类持有实例(public static Singleton getInstance() { return Holder.INSTANCE; }private static class Holder { static final Singleton INSTANCE = new Singleton(); } 线程安全、懒加载(内部类在首次调用时加载)、无同步开销 推荐替代双重检查锁定的简洁实现
枚举单例 通过枚举类型定义实例(public enum Singleton { INSTANCE; } 绝对线程安全、防反射/反序列化攻击(JVM保证枚举实例唯一) 需严格防破坏的单例场景

最佳实践

建议 理由
优先使用静态内部类 代码简洁,利用JVM类加载机制实现线程安全的懒加载,无同步开销,是生产环境推荐方案。
枚举单例防破坏 若需严格防止反射攻击或反序列化破坏单例(如安全敏感场景),使用枚举类型(enum)。
私有构造方法防御反射 在私有构造方法中添加检查(如if (instance != null) throw new RuntimeException("单例实例已存在");),防止反射调用。
处理序列化问题 若单例类需序列化,重写readResolve()方法(protected Object readResolve() throws ObjectStreamException { return getInstance(); }),避免反序列化创建新实例。
明确初始化参数 首次调用getInstance()时必须传入必要参数(如日志文件路径),后续调用忽略参数,确保实例状态正确。
避免滥用单例 仅当对象需全局唯一时使用单例模式,否则可能导致代码耦合(如将业务逻辑与单例绑定,难以测试)。

一句话总结

单例模式通过控制类仅创建一个实例并提供全局访问点,解决了资源浪费、状态不一致等问题,是实现全局唯一对象的高效方案。

如果关注Java设计模式内容,可以查阅作者的其他Java设计模式系列文章。😊


网站公告

今日签到

点亮在社区的每一天
去签到