设计模式-Singleton

119 阅读3分钟

创建型模式

问题

在某些场景下,需要保证一个类只有一个实例,并提供一个访问它的全局访问点。这个时候就需要使用单例模式。

组成

单例模式的组成如下所示:

和其他设计模式的 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 事件后再启动线程池。

小结

单例模式适用于解决的问题是:全局希望某个类只有一个实例,且该类提供这个唯一实例的创建和全局入口。