模式说明

单例的意思就是只有一个实例。单例是仅实例化一次的类,为了避免并发问题,单例对象必须是无状态,单例的优势在于控制资源及节省内存。值得注意的是,创建单例的操作在单例类内部完成,单例类的构造方法必须私有且不能够被反序列化。

单例模式的缺点在于对单元测试不友好,因为对单例实现模拟测试会变得困难,但也并非完全没有办法解决,可以通过依赖注入的方式传参或定制上下文对象获取单例的方式(相当于二次封装)模拟该单例的行为。因此,单例对象在框架中使用并不常见,但有些类本身就非常适合作为单例对象,所以在应用层上的使用比较广泛。

应用场景

对于应用里的配置资源,我们可以让配置资源对象以单例形式存在,可以节省内存空间,这样的还有另外一个好处是加载配置、重载配置、获取配置都会变得更加方便和加锁之后使得配置资源操作更加安全。还有无状态的工具类也很适合通过单例模式实现,如对象转换,将 JSON 转为 Map 等。

对象池的统一管理也适合使用单例模式,如 Java 里的 ForkJoinPool.commonPool(),还有的是数据库连接池或 HTTP 连接池等,都可以通过单例对象进行管理。

还有应用级别的全局对象 Application(如 Tomcat 中的 ApplicationContext)这样就可以统一管理所有资源,而那些被管理的资源对象就不需要再要求以单例形式存在了。

值得注意的是,如果一个实例对象可以在运行时改变其内部的变量值,这种对象就存在状态,状态的改变将会导致引用该单例对象的函数逻辑变得前后不一致。所以,存在状态的类不建议使用单例模式实现,除非实例状态在创建后不可变。

资源管理相关的单例,大概就如下图 1 所示。
image.png
图 1

实现方式

1. 饿汉(推荐)

很多时候,饿汉的单例实现方式就够用了,当只有使用 ConfigManager.CONFIG_MANAGER 的时候才会对 ConfigManager 对象进行初始化操作,而且 JVM 内部存在锁控制,使得在多线程情况下避免并发问题。

  1. public class ConfigManager {
  2. public static final ConfigManager CONFIG_MANAGER = new ConfigManager();
  3. private ConfigManager() {
  4. // load configuration
  5. }
  6. }

值得注意的是,虽然只有使用的时候才会初始化,但这里的使用不仅仅是使用 CONFIG_MANAGER 对象,还有其他非常量池中的对象使用也会对 ConfigManager 进行初始化操作,如下代码所示。

  1. public class ConfigManager {
  2. public static final ConfigManager CONFIG_MANAGER = new ConfigManager();
  3. public static final int LOAD_TIMEOUT = 10; // 10s
  4. public static final Pattern EXCLUED_NODES = Pattern.compile("^.*temp.*$");
  5. private ConfigManager() {
  6. System.out.println("Configuration loaded");
  7. }
  8. }
  9. int timeout = ConfigManager.LOAD_TIMEOUT; // 不会触发 ConfigManager 初始化
  10. // 不打印 Configuration loaded
  11. Pattern pattern = ConfigManager.EXCLUED_NODES; // 触发 ConfigManager 初始化
  12. // 打印 Configuration loaded

因为在编译的时候,int timeout = ConfigManager.LOAD_TIMEOUT 直接就变成 int timeout = 10,因此相当于没有使用 ConfigManager 对象。

更好的方式是,基于在“饿汉”基础上增加静态工厂方法,代码如下所示。

  1. public class ConfigManager {
  2. private static final ConfigManager CONFIG_MANAGER = new ConfigManager();
  3. private ConfigManager() {
  4. // load configuration
  5. }
  6. public static ConfigManager getInstance() {
  7. return CONFIG_MANAGER;
  8. }
  9. }

2. 懒汉

“懒汉”的实现方式是在实际调用 ConfigManager.getInstance() 时才会初始化 ConfigManager 类,这其实与“饿汉”方式类似,但因为需要控制为单一实例,因此也加上了同步的修饰符(synchronized),避免并发问题,而恰恰又引入了性能问题。

  1. public class ConfigManager {
  2. private static ConfigManager configManager;
  3. private ConfigManager() {
  4. // load configuration
  5. }
  6. public static synchronized ConfigManager getInstance() {
  7. if (configManager == null) {
  8. configManager = new ConfigManager();
  9. }
  10. return configManager;
  11. }
  12. }

3. 静态内部类

如果因为种种原因,无法使得单例类保持单一,且初始化该单例类的代价比较高昂,那就推荐使用静态内部类的方式,只有在调用 ConfigManager.getInstance() 的时候才会真正对 ConfigManager 进行初始化操作,而且方法上还没有同步修饰符,是一个真正的延迟加载方式。

  1. public class ConfigManager {
  2. private ConfigManager() {
  3. // load configuration
  4. }
  5. private static class ConfigMangerHolder {
  6. private static final ConfigManager CONFIG_MANAGER = new ConfigManager();
  7. }
  8. public static ConfigManager getInstance() {
  9. return ConfigMangerHolder.CONFIG_MANAGER;
  10. }
  11. }

4. 双重检查

通过两次判断对象是否为 null 的方式实现延迟加载,为了避免指令重排,需要增加 volatile 修饰符,但高版本的 JVM 已经修复了这个指令重排的问题。

  1. public class ConfigManager {
  2. private static volatile ConfigManager configManager;
  3. private ConfigManager() {
  4. // load configuration
  5. }
  6. public static ConfigManager getInstance() {
  7. if (configManager == null) {
  8. synchronized (ConfigManager.class) {
  9. if (configManager == null) {
  10. configManager = new ConfigManager();
  11. }
  12. }
  13. }
  14. return configManager;
  15. }
  16. }

5. 枚举

上面 #1 ~ #4 的实现方式中并未考虑反序列化的时候重复创建实例导致单例模式失效的问题,因此,如果需要考虑避免反序列化的问题,那我们就可以通过枚举的方式实现单例,代码如下。

  1. public enum ConfigManager {
  2. INSTANCE;
  3. ConfigManager() {
  4. // load configuration
  5. }
  6. }

显然,这种实现方式有点反直觉了,因此我个人还是不建议这样实现。