创建型模式
问题
在某些场景下,需要保证一个类只有一个实例,并提供一个访问它的全局访问点。这个时候就需要使用单例模式。
组成
单例模式的组成如下所示:
和其他设计模式的 UML 相比,单例模式没有复杂的继承接口,一般只有一个需要保证单例的类 Singleton,它持有一个静态对象 INSTANCE、提供一个静态方法 instance()、private 或 protected 修饰的构造器方法:
- INSTANCE:全局唯一的 Singleton 实例引用
- instance():返回 INSTANCE
- 私有构造器:保证不会创建更多的实例
客户只需要通过 Singleton 的 instance() 方法来获得唯一的实例引用。
应用场景
有些时候,在整个系统中某些类只需要一个实例,或者只能有一个实例,比如 DDD 架构服务中的事件 Publisher 只需要一个,Spring 框架下一个 bean 只能有一个,在这些场景下就需要使用单例模式。
在使用单例模式时,就要考虑两个问题:
- 如何保证全局只有一个实例?
- 如何保证这个唯一的实例在全局被访问?
最好的办法是,让类自身负责它的唯一实例(创建、持有),这个类的构造器是 private/protected 修饰的,不会创建更多的实例,并且这个类提供该唯一实例的访问(持有静态引用、提供静态方法)。或者像 Spring 那样,提供一个容器管理所有的 bean,保证每个 bean 都是唯一的。
示例代码
单例模式是应届生找工作、实习面试时,基本必问的一个设计模式,它可以和并发编程一起,变种为“怎么在多线程下保证实例唯一、避免 synchronized 的性能问题”,已经有很多文章解释了,这里就不介绍了,下面只贴一下双重检测、饿汉式的代码:
版本 1:懒汉式、双重检测
class Singleton {
private static volatile Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() {
if(INSTANCE == null) {
synchronized (Singleton.class) {
if(INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
版本 2:饿汉式
这种方式代码简单,在类加载时就会创建实例,保证了线程安全。
class Singleton {
private static final Singleton INSTANCE;
static {
INSTANCE = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
DDD 事件 Publisher
在我们的项目开发中,我们使用了一个 DDD 事件 Publisher,主要代码如下:
public class DomainEventPublisher {
private volatile static DomainEventPublisher INSTANCE;
public DomainEventPublisher(DomainEventInternalPublisher internalPublisher,
DomainEventRepository domainEventRepository,
DomainEventExternalPublisher externalPublisher) {
this.internalPublisher = internalPublisher;
this.domainEventRepository = domainEventRepository;
this.externalPublisher = externalPublisher;
if (INSTANCE == null) {
synchronized (DomainEventPublisher.class) {
if (INSTANCE != null) {
throw new IllegalStateException();
}
INSTANCE = this;
}
}
}
public static DomainEventPublisher instance() {
return INSTANCE;
}
// DomainEvent 是 领域事件的抽象类
public <T extends DomainEvent> void publish(final T domainEvent) {
}
}
使用的时候,只需要执行以下调用即可发出一个事件:
DomainEventPublisher.instance().publish(new ConcreteDomainEvent());
在这里就使用单例模式,但存在一个问题:如果它依赖其他的构造器依赖其他 spring bean,在自身的 bean 被 spring 注入之前,如果被调用 publish 方法的话,就会抛出空指针异常(NPE)。
在实际使用过程,我们也的确遇到了这个问题:另一个组件中的线程池使用 DomainEventPublisher 发出事件,但此时 Spring 还没注入该 bean,抛出了 NPE。最后的解决方法是,另一组件通过监听 Spring 的 ContextRefreshedEvent 事件后再启动线程池。
小结
单例模式适用于解决的问题是:全局希望某个类只有一个实例,且该类提供这个唯一实例的创建和全局入口。