设计模式——单例设计模式(创建型)

摘要

本文详细介绍了单例设计模式,包括其定义、结构、实现方法及适用场景。单例模式是一种创建型设计模式,确保一个类只有一个实例并提供全局访问点。其要点包括唯一性、私有构造函数、全局访问点和线程安全。文章还展示了单例设计模式的类图和时序图,并介绍了三种实现方式:饿汉式、静态内部类和枚举方式。最后列举了单例模式适合和不适合的场景,以及实战建议和示例,如配置中心、统一 ID 生成器、日志收集器等。

1. 单例设计模式定义

单例模式 是一种创建型设计模式,其目的是确保一个类只有一个实例,并提供一个全局访问点来获取该实例

通俗理解:

  • 单例模式就是让一个类只创建一个对象,就像系统中只能有一个"总统"或"日志管理器"。
  • 这个类自己控制这个唯一实例的创建,并且其他类只能通过它提供的方法来获取这个对象。

|--------------|-------------------|
| 要点 | 说明 |
| 唯一性 | 类只能有一个实例 |
| 私有构造函数 | 禁止外部直接用 new创建对象 |
| 全局访问点 | 提供一个静态方法获取该实例 |
| 线程安全(可选) | 在多线程环境下仍能保持唯一性 |

2. 单例设计模式结构

2.1. 单例设计模式类图

2.2. 单例设计模式时序图

3. 单例设计模式实现方式

所有单例的实现都包含以下两个相同的步骤:

  1. 将默认构造函数设为私有, 防止其他对象使用单例类的 new运算符。
  2. 新建一个静态构建方法作为构造函数。 该函数会 "偷偷" 调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。

如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。

|--------------------|--------|-------|-----------------|
| 实现方式 | 是否线程安全 | 是否懒加载 | 推荐程度 |
| 饿汉式 | 是 | 否 | ✅ 推荐(简单可靠) |
| 懒汉式(线程不安全) | 否 | 是 | ❌ 不推荐 |
| 懒汉式 + synchronized | 是 | 是 | ⚠️ 有性能开销 |
| 双重检查锁(DCL) | 是 | 是 | ✅ 推荐(兼顾性能) |
| 静态内部类 | 是 | 是 | ✅ 推荐(懒加载 + 安全) |
| 枚举方式 | 是 | 否 | ✅ 最推荐(防反射、反序列化) |

3.1. 📍 饿汉式(推荐)

复制代码
public class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {} // 构造器私有化

    public static Singleton getInstance() {
        return instance;
    }
}

3.2. 📍 静态内部类(推荐)

复制代码
public class Singleton {
    private Singleton() {}

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

3.3. 📍 枚举方式(最安全)

复制代码
public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("do...");
    }
}

4. 单例设计模式适合场景

4.1. ✅ 单例模式适合的场景

|-----------|--------------------------|---------------------------------------------------|
| 场景类别 | 说明 | 示例 |
| 配置管理类 | 系统中读取一次后多处使用,需全局共享 | AppConfig.getInstance().get("db.url") |
| 日志系统 | 全局统一记录日志,防止多个文件或实例导致管理混乱 | Logger.getInstance().log("...") |
| 线程池 / 连接池 | 统一管理资源,避免重复创建、浪费连接 | DbConnectionPool.getInstance().getConnection() |
| 任务调度器 | 控制任务执行的唯一调度入口 | TaskScheduler.getInstance().schedule(task) |
| 唯一 ID 生成器 | 全局 ID 要求唯一,需中心化生成 | IdGenerator.getInstance().nextId() |
| 系统监控模块 | 全局收集监控信息,避免多个统计点造成数据不一致 | MetricsCollector.getInstance().record("qps", 5) |

4.2. ❌ 单例模式不适合的场景

|------------|-------------------------------------|---------------------------------------------|
| 场景类别 | 问题描述 | 示例或说明 |
| 会话/用户状态类 | 多用户或请求需独立状态,单例可能造成状态串扰或数据混乱。 | 用户登录状态、购物车信息等 |
| 多实例业务模型 | 业务本身设计要求一个类存在多个不同实例 | 订单、交易、商品等 |
| 单元测试场景 | 单例难以隔离状态,不利于并发测试和 mock。 | 单例残留状态会污染其他测试 |
| 生命周期绑定业务对象 | 对象需按请求或事务创建销毁,单例不符合生命周期需求。 | HTTP 请求上下文、数据库事务上下文 |
| 状态频繁变化类 | 状态共享会导致线程不安全,需加锁处理复杂性上升。 | 非线程安全的缓存组件、计算任务执行状态 |
| 需依赖注入管理的类 | 单例可能和 Spring 等框架的容器管理冲突,影响可测试性和解耦性。 | 建议用 Spring Bean 单例管理(@Component + @Scope) |

4.3. 🧠 单例设计模式实战建议

|--------------------|----------|----------------------|
| 使用场景 | 是否推荐使用单例 | 说明 |
| 配置类 / 常量类 | ✅ 是 | 全局唯一即可 |
| Controller/Service | ❌ 否 | 由 Spring 容器管理生命周期更合适 |
| 每个用户/请求有状态 | ❌ 否 | 应使用原型模式或线程隔离 |
| 工具类(无状态) | ⚠️ 视情况 | 可以用 static 工具类替代 |

5. 单例设计模式实战示例

在 Spring 项目中,单例模式(Singleton Pattern)使用场景非常广泛。Spring 容器管理的 Bean 默认就是单例模式 ,它本质上满足了单例设计模式的定义:"确保一个类只有一个实例,并且提供一个全局访问点。所以在 Spring 项目中,我们一般直接使用 Spring 单例 Bean,既符合单例设计模式的定义,又简化了开发和维护的复杂度。下面是一些常见 适合使用单例的场景 及其在 Spring 中的实现示例:

|----------------------|------------------------------------------------------|
| 设计模式核心要求 | Spring Bean 示例体现方式 |
| 唯一实例(Singleton) | Spring 容器中该 Bean 默认只实例化一次,所有注入该 Bean 的地方都共享同一个实例。 |
| 全局访问点 | 通过 Spring 的依赖注入(@Autowired)或获取 Bean 的方式,全局访问同一个实例。 |
| 控制实例创建(防止多次 new) | 不用 new 直接调用构造器,而是由 Spring 容器负责实例创建和生命周期管理。 |
| 线程安全(视具体实现而定) | 需要保证成员变量线程安全,比如用线程安全的数据结构或无状态设计。 |

5.1. ✅ 配置中心 / 配置管理器

使用场景: 需要在系统中读取一次配置,供全局使用。

复制代码
@Component
@Data
public class AppConfig {
    
    @Value("${app.env}")
    private String env;
}

使用方式:

复制代码
@Service
public class MyService {
    
    @Autowired
    private AppConfig appConfig;

    public void doSomething() {
        System.out.println(appConfig.getEnv());
    }
}

5.2. ✅ 统一 ID 生成器(如雪花算法)

复制代码
@Component
public class IdGenerator {

    private final AtomicLong counter = new AtomicLong();

    public long nextId() {
        return counter.incrementAndGet();
    }
}

使用方式:

复制代码
@Service
public class OrderService {
    
    @Autowired
    private IdGenerator idGenerator;

    public void createOrder() {
        Long orderId = idGenerator.nextId();
        // 创建订单逻辑
    }
}

5.3. ✅ 日志收集器 / 监控埋点上报器

复制代码
@Component
public class MetricsCollector {
    
    public void record(String metric, int value) {
        // 上报指标逻辑
        System.out.println("metric: " + metric + " value: " + value);
    }
}

使用方式:

复制代码
@Service
public class PaymentService {
    
    @Autowired
    private MetricsCollector metricsCollector;

    public void pay() {
        // 业务逻辑
        metricsCollector.record("payment.count", 1);
    }
}

5.4. ✅ 缓存组件(轻量场景)

复制代码
@Component
public class LocalCache {
    
    private final Map<String, Object> cache = new ConcurrentHashMap<>();

    public void put(String key, Object value) {
        cache.put(key, value);
    }

    public Object get(String key) {
        return cache.get(key);
    }
}

5.5. ✅ 线程池 / 异步任务执行器(通过 Spring 管理)

复制代码
@Configuration
public class ThreadPoolConfig {

    @Bean
    public Executor taskExecutor() {
        return Executors.newFixedThreadPool(10);
    }
}

使用方式:

复制代码
@Service
public class AsyncTaskService {

    @Autowired
    private Executor taskExecutor;

    public void runAsyncTask() {
        taskExecutor.execute(() -> System.out.println("Running async task"));
    }
}

5.6. ✅ 策略工厂 / 状态机容器

这些模式本质上也是通过单例注册机制实现的,通常用 @Component + Map<String, Strategy> 组合来做策略路由。

复制代码
@Component
public class StrategyFactory {
    
    private final Map<String, Strategy> strategies;

    public StrategyFactory(List<Strategy> strategyList) {
        strategies = new HashMap<>();
        for (Strategy s : strategyList) {
            strategies.put(s.getType(), s);
        }
    }

    public Strategy get(String type) {
        return strategies.get(type);
    }
}

5.7. ✅ Spring 项目中适合单例的场景

|---------|-----------------------------|-----------|
| 场景名称 | Spring 推荐实现 | 是否线程安全 |
| 配置类 | @Component+ @Value | ✅ 是 |
| ID 生成器 | @Component+ 原子类 | ✅ 是 |
| 日志/监控工具 | @Component+ 线程安全方法 | ✅ 是 |
| 缓存组件 | @Component+ ConcurrentMap | ✅ 是(注意并发) |
| 工具类 | @Componentstatic工具类 | ⚠️ 视情况 |

6. 单例设计模式思考

6.1. 为什么spring中对象天然是单例?

6.1.1. Spring 容器设计初衷

  • Spring 是一个IoC(控制反转)容器,负责管理应用中的对象生命周期和依赖关系。
  • 容器初始化时,会根据配置(注解或 XML)创建并管理 Bean 实例。
  • 默认情况下,Spring 容器只会创建一个共享的 Bean 实例,供所有依赖该 Bean 的组件共享使用。

6.1.2. 单例 Bean 的定义和作用域

  • Spring 中的单例是指在 Spring 容器中只有一个实例,而不是 JVM 层面上的全局单例。
  • 默认作用域是 singleton,即:每个 Spring 容器中该 Bean 只有一个实例
  • 你可以通过 @Scope("prototype") 等其他作用域来改变默认行为。

6.1.3. Spring 单例实现机制(简要)

  • 容器启动时,会扫描并实例化所有单例 Bean。
  • 创建后,将实例放入一个单例缓存池(例如 singletonObjects)。
  • 当其他组件请求该 Bean 时,直接从缓存池取,避免重复创建。
  • 通过这种方式,Spring 确保每个 Bean 在容器内是唯一的。

6.1.4. 为什么默认使用单例?

  • 节省资源:不必每次调用都创建新实例,减少内存开销。
  • 方便共享:多个组件可以共享状态或行为一致的对象。
  • 生命周期管理:由容器统一管理,便于统一销毁或初始化。
  • 线程安全的前提下,提高性能:一般单例 Bean 设计为无状态或线程安全,避免多次实例化开销。

6.1.5. 需要注意的点

  • Spring 的单例是"容器单例",不同的 Spring 容器可以有不同的实例。
  • 如果使用多个容器或类加载器,则可能出现多实例。
  • 单例 Bean 设计时应注意线程安全,避免可变状态带来的并发问题。
  • 业务中有状态的 Bean 一般不要用单例,使用 prototype 或其他作用域。

6.2. Spring 的单例是"容器单例",不同的 Spring 容器可以有不同的实例。

意思是: Spring 单例不是 JVM 层面全局的单例,而是"每个 Spring 容器(ApplicationContext)中唯一的实例"。如果你项目里启动了多个 Spring 容器 (比如多个 ApplicationContext 实例),每个容器都会单独创建自己的那个 Bean 实例。

举例:

  1. 你有两个 Web 应用,每个运行一个 Spring 容器,它们各自有自己的单例 Bean 实例。
  2. 或者你启动了多个 Spring 容器做测试、隔离等,也会有多个实例。

6.3. 如果使用多个容器或类加载器,则可能出现多实例

类加载器(ClassLoader)不同,虽然类名相同,但被 JVM 认为是不同的类。因此,如果你在不同的类加载器中加载同一个类,也会导致出现"多个单例实例",不是同一个对象。

典型场景:

  • Java EE 容器中不同的部署单元(war包、ear包)
  • 插件式架构、模块化系统
  • 热部署(热更新)时重载类

6.4. 单例Bean设计时应注意线程安全,避免可变状态带来的并发问题

Spring 单例Bean是被多个线程共享的(特别是 Web 应用中,多个请求同时访问)。如果单例 Bean 内部有可变的成员变量,就会有线程安全风险,可能导致数据错乱或异常。

设计原则:

  • 无状态设计: Bean 不保存业务状态,所有状态通过方法参数传递。
  • 线程安全的数据结构: 比如使用 ConcurrentHashMapAtomicInteger
  • 同步控制: 必要时用锁、synchronized 保证并发安全。

6.5. 业务中有状态的 Bean 一般不要用单例,使用 prototype 或其他作用域

有状态 Bean:保存用户会话、操作数据等状态的 Bean。用单例的话,状态被多个线程共享,会导致状态混乱和并发问题。这时应使用 Spring 的其他作用域,比如:

  • prototype:每次请求都会创建新实例,避免共享状态。
  • request(Web作用域):每个 HTTP 请求一个实例。
  • session:每个用户会话一个实例。

博文参考

相关推荐
冰茶_4 小时前
适配器模式:让不兼容接口协同工作
microsoft·设计模式·适配器模式
magic 2454 小时前
Java设计模式详解:策略模式(Strategy Pattern)
java·设计模式·策略模式
琢磨先生David5 小时前
从模式到架构:Java 工厂模式的设计哲学与工程化实践
java·设计模式
庄小焱6 小时前
设计模式——状态设计模式(行为型)
设计模式
庄小焱6 小时前
设计模式——责任链设计模式(行为型)
设计模式
勤奋的知更鸟6 小时前
Java抽象工厂模式详解
设计模式
季鸢7 小时前
Java设计模式之迭代器模式详解
java·设计模式·迭代器模式
NorthCastle8 小时前
设计模式-行为型模式-模版方法模式
java·设计模式·模板方法模式
庄小焱11 小时前
设计模式——观察者设计模式(行为型)
设计模式
暴躁哥11 小时前
深入理解设计模式之解释器模式
python·设计模式·解释器模式