【设计模式】单例模式

2022/03/30 设计模式 共 2945 字,约 9 分钟

随便问一个程序员,让他说一说最熟悉的 3 种设计模式,单例模式肯定是其中一个。

那么,我们真的理解单例模式了吗?下面我们围绕以下几个问题展开:

  • 为什么要使用单例?
  • 单例存在哪些问题?
  • 单例与静态类的区别?
  • 有何替代的解决方案?

为什么使用单例

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

为什么我们需要单例这种设计模式?它能解决哪些问题?

处理资源访问冲突

我们实现一个日志打印类Logger


public class Logger {
  private FileWriter writer;
  
  public Logger() {
    File file = new File("/Users/dxyin/log.txt");
    writer = new FileWriter(file, true); //true表示追加写入
  }
  
  public void log(String message) {
    writer.write(message);
  }
}

// Logger类的应用示例:
public class UserController {
  private Logger logger = new Logger();
  
  public void login(String username, String password) {
    // ...省略业务逻辑代码...
    logger.log(username + " logined!");
  }
}

其他类在同时调用log()方法,会发生日志覆盖的情况,也就是会发生访问资源冲突。

当然,我们可以在log()方法上加上类锁,相对而言,单例模式的解决思路就简单一些了。单例模式相对于之前类级别锁的好处是,不用创建那么多 Logger 对象,节省内存和其他资源。

表示全局唯一类

从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。

比如,配置信息类,在系统中只有一份,比如还有唯一递增 ID 号码生成器等。

此处省略代码,具体实现见单例的多种实现方式。

如何实现单例

要实现一个单例,我们需要关注的点无外乎下面几个:

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

饿汉式

饿汉式的实现方式比较简单。在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。

// Id生成器
public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  // 静态实例,已初始化
  private static final IdGenerator instance = new IdGenerator();
  //私有构造器
  private IdGenerator() {}
  // 静态获取实例
  public static IdGenerator getInstance() {
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

缺点是这样的实现方式不支持延迟加载。

懒汉式

懒汉式相对于饿汉式的优势是支持延迟加载。

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private IdGenerator() {}
  // 在获取实例的时候才加载
  public static synchronized IdGenerator getInstance() {
    if (instance == null) {
      instance = new IdGenerator();
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

缺点是加了一把大锁,并发度低,如果频繁获取实例,性能很低。

双重检测

我们再来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式。

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private IdGenerator() {}
  
  public static IdGenerator getInstance() {
    // 第一次检测,如果实例不存在,加上类锁
    if (instance == null) {
      synchronized(IdGenerator.class) {
        // 第二次检测
        if (instance == null) {
          instance = new IdGenerator();
        }
      }
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

静态内部类

比双重检测更加简单的实现方法,那就是利用 Java 的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private IdGenerator() {}

  // 静态内部类,含静态instance,延迟加载
  private static class SingletonHolder{
    private static final IdGenerator instance = new IdGenerator();
  }
  
  // 获取实例是从静态内部类获取
  public static IdGenerator getInstance() {
    return SingletonHolder.instance;
  }
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

枚举

最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

public enum IdGenerator {
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

单例存在的问题

  • 单例对 OOP 特性的支持不友好
  • 单例会隐藏类之间的依赖关系
  • 单例对代码的扩展性不友好
  • 单例对代码的可测试性不友好
  • 单例不支持有参数的构造函数

替代方案

如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。

实际上,类对象的全局唯一性可以通过多种不同的方式来保证。

我们既可以通过单例模式来强制保证,也可以通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证。

总结

单例模式饿汉式懒汉式双重检测静态内部类枚举
原理类加载的时候,instance 静态实例就已经创建并初始化获取实例的时候加对象锁获取实例的时候两次检测增加一个静态内部类去实例化枚举类
优点简单,线程安全延迟加载性能高、延迟加载线程安全、延迟加载简单
缺点不支持延迟加载性能低   

文档信息

搜索

    Table of Contents