定义

单例设计模式(Singleton Design Pattern),如果一个类只允许创建一个对象(或者叫实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
用途:

  • 处理资源访问冲突
  • 表示全局唯一类

要实现一个单例,关注的点有:

  • 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;
  • 考虑对象创建时的线程安全问题;
  • 考虑是否支持延迟加载;
  • 考虑 getInstance() 性能是否高(是否加锁)。


结构

主要角色

  • 单例类:包含一个实例且能自行创建这个实例的类。
  • 访问类:使用单例的类。

    结构图

    单例模式(Singleton Pattern) - 图1

应用

ID生成器

饿汉式

  1. public class IdGenerator {
  2. private final AtomicLong id = new AtomicLong(0);
  3. private static final IdGenerator instance = new IdGenerator();
  4. private IdGenerator() {}
  5. public static IdGenerator getInstance() {
  6. return instance;
  7. }
  8. public long getId() {
  9. return id.incrementAndGet();
  10. }
  11. }

简单高效,但是不支持延迟加载。

懒汉式

  1. public class IdGenerator {
  2. private final AtomicLong id = new AtomicLong(0);
  3. private static IdGenerator instance;
  4. private IdGenerator() {}
  5. public static synchronized IdGenerator getInstance() {
  6. if (instance == null) {
  7. instance = new IdGenerator();
  8. }
  9. return instance;
  10. }
  11. public long getId() {
  12. return id.incrementAndGet();
  13. }
  14. }

延迟加载,但要加锁保证线程安全,并发度低。

双重检测

  1. public class IdGenerator {
  2. private AtomicLong id = new AtomicLong(0);
  3. private static IdGenerator instance;
  4. private IdGenerator() {}
  5. public static IdGenerator getInstance() {
  6. if (instance == null) { // 使用双重检测提高并发效率
  7. synchronized(IdGenerator.class) { // 此处为类级别的锁
  8. if (instance == null) {
  9. instance = new IdGenerator();
  10. }
  11. }
  12. }
  13. return instance;
  14. }
  15. public long getId() {
  16. return id.incrementAndGet();
  17. }
  18. }

关于指令重排:
因为指令重排序,可能会导致 IdGenerator 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。要解决这个问题,我们需要给 instance 成员变量加上 volatile 关键字,禁止指令重排序才行。实际上,只有很低版本的 Java 才会有这个问题。我们现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)。

静态内部类

  1. public class IdGenerator {
  2. private AtomicLong id = new AtomicLong(0);
  3. private IdGenerator() {}
  4. private static class SingletonHolder{
  5. private static final IdGenerator instance = new IdGenerator();
  6. }
  7. /**
  8. * 只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,
  9. * 这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。
  10. */
  11. public static IdGenerator getInstance() {
  12. return SingletonHolder.instance;
  13. }
  14. public long getId() {
  15. return id.incrementAndGet();
  16. }
  17. }

利用 Java 的静态内部类来实现单例。这种实现方式,既支持延迟加载,也支持高并发,实现起来也比双重检测简单。

枚举


  1. public enum IdGenerator {
  2. INSTANCE;
  3. private AtomicLong id = new AtomicLong(0);
  4. public long getId() {
  5. return id.incrementAndGet();
  6. }
  7. }

在spring中的应用

environment,systemProperties,systemEnvironment,单例bean(容器范围)

优点

减少了内存的开销

单例模式可以保证内存里只有一个实例,减少了内存的开销。

全局访问点

单例模式设置全局访问点,可以优化和共享资源的访问。

缺点

单例对 OOP 特性的支持不友好

一旦选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性。

单例会隐藏类之间的依赖关系

通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义,就能很容易识别出来。但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。

单例对代码的扩展性不友好

单例可能会随需求变化需要多个实例

单例不支持有参数的构造函数

解决办法:

  • 提供init(),在getInstance()前调用
  • getInstance时传入(蛋疼)
  • 将参数放到另外的全局变量,或从配置文件读取(推荐)

扩展

单例模式的一种优化

  1. // 类似依赖注入
  2. public demofunction(IdGenerator idGenerator) {
  3. long id = idGenerator.getId();
  4. }
  5. // 外部调用demofunction()的时候,传入idGenerator
  6. IdGenerator idGenerator = IdGenerator.getInsance();
  7. demofunction(idGenerator);

可以解决单例隐藏类之间依赖关系的问题。不过,对于单例存在的其他问题,比如对 OOP 特性、扩展性、可测性不友好等问题,还是无法解决。

单例模式的替代方案

  • 工厂模式
  • IOC容器

单例的唯一性

进程唯一

即普通的单例模式,对于 Java 语言来说,单例类对象的唯一性的作用范围并非进程,而是类加载器(Class Loader)

线程唯一的单例


  1. public class IdGenerator {
  2. private AtomicLong id = new AtomicLong(0);
  3. private static final ConcurrentHashMap<Long, IdGenerator> instances
  4. = new ConcurrentHashMap<>();
  5. private IdGenerator() {}
  6. public static IdGenerator getInstance() {
  7. Long currentThreadId = Thread.currentThread().getId();
  8. instances.putIfAbsent(currentThreadId, new IdGenerator());
  9. return instances.get(currentThreadId);
  10. }
  11. public long getId() {
  12. return id.incrementAndGet();
  13. }
  14. }

ThreadLocal原理类似,使用的是ThreadLocalMap来实现

集群唯一的单例

将单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁。


public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private static SharedObjectStorage storage = FileSharedObjectStorage(/*入参省略,比如文件地址*/);
  private static DistributedLock lock = new DistributedLock();

  private IdGenerator() {}

  public synchronized static IdGenerator getInstance() 
    if (instance == null) {
      lock.lock();
      instance = storage.load(IdGenerator.class);
    }
    return instance;
  }

  public synchroinzed void freeInstance() {
    storage.save(this, IdGeneator.class);
    instance = null; //释放对象
    lock.unlock();
  }

  public long getId() { 
    return id.incrementAndGet();
  }
}

// IdGenerator使用举例
IdGenerator idGeneator = IdGenerator.getInstance();
long id = idGenerator.getId();
IdGenerator.freeInstance();

多例模式

按数量


public class BackendServer {
  private long serverNo;
  private String serverAddress;

  private static final int SERVER_COUNT = 3;
  private static final Map<Long, BackendServer> serverInstances = new HashMap<>();

  static {
    serverInstances.put(1L, new BackendServer(1L, "192.134.22.138:8080"));
    serverInstances.put(2L, new BackendServer(2L, "192.134.22.139:8080"));
    serverInstances.put(3L, new BackendServer(3L, "192.134.22.140:8080"));
  }

  private BackendServer(long serverNo, String serverAddress) {
    this.serverNo = serverNo;
    this.serverAddress = serverAddress;
  }

  public BackendServer getInstance(long serverNo) {
    return serverInstances.get(serverNo);
  }

  public BackendServer getRandomInstance() {
    Random r = new Random();
    int no = r.nextInt(SERVER_COUNT)+1;
    return serverInstances.get(no);
  }
}

按类型


public class Logger {
  private static final ConcurrentHashMap<String, Logger> instances
          = new ConcurrentHashMap<>();

  private Logger() {}

  public static Logger getInstance(String loggerName) {
    instances.putIfAbsent(loggerName, new Logger());
    return instances.get(loggerName);
  }

  public void log() {
    //...
  }
}

//l1==l2, l1!=l3
Logger l1 = Logger.getInstance("User.class");
Logger l2 = Logger.getInstance("User.class");
Logger l3 = Logger.getInstance("Order.class");