浅谈单例模式

单例模式用于确保一个类只有一个实例,并提供一个全局访问点。

一般使用场景

  1. 日志:单例日志记录器用于将消息记录到文件中。
  2. 数据库连接:单例数据库连接用于连接到数据库。
  3. 配置:单例配置对象用于存储应用程序配置。
  4. 缓存:单例缓存用于存储应用程序数据。

如何实现单例模式

public class Singleton {
    // data fields
    // ...
    private static final Singleton instance = new Singleton();
    private Singleton() {
    }
    public static Singleton getInstance() {
        return instance;
    }

    // more public methods
   public void doSomething() {
   }
}

这意味着在加载类时创建一个静态单例实例。

这是最常见的实现,但它的内存效率不高。如果不使用类,单例仍然会被创建,如果单例很重,它将消耗大量内存。

让我们来看看创建单例时的一些重要问题。

  1. 线程安全:因为单例是为多个线程创建的,所以它们需要线程安全。线程安全可以从两方面来看待。

    1. 确保实例不会被创建吵过一次
    2. 如果单例对象保存数据,就像在缓存中一样,确保数据是线程安全的。更新数据的方法应该同步。
  2. 效率:我们需要确保最佳的内存使用和性能

    1. 内存泄漏:如果单例很重且不使用,它将消耗大量内存
    2. 资源使用:如果单例模式消耗系统资源,它将消耗大量的CPU周期。在这种情况下,除非使用单例,否则系统将得不到充分利用。
    3. 序列化和反序列化:如果单例被序列化和反序列化,它将被重新创建,在这种情况下将创建多个实例。然而,序列化单例并不常见,理解这一点很重要

所以让我们看看上述的单例实现是否考虑到了这些问题。

  1. 在实例创建方面是线程安全的——是的,因为在加载类时只创建一次静态实例。
  2. 内存使用不是最优的,因为单例是在加载类时创建的,而不是在使用类时创建的。
  3. 不会阻止单例对象被序列化和反序列化。

现在我们来逐个看看其他的方法。

私有静态实例与惰性初始化

public class Singleton {
    private static Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

更多的内存效率。

这意味着在第一次访问该单例对象之前,它不会被创建。

然而,这并没有考虑到其他问题,并且失去了线程安全性

私有静态实例与同步的getInstance()方法

public class Singleton {
    private static Singleton instance = null;
    private Singleton() {
    }
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

更加有效,并且是线程安全的。

这解决了多个线程试图同时创建单例的问题。然而,这也有一个小小的权衡。同步方法会使单例模式变慢,因为如果另一个线程正在请求该单例模式,同步方法会阻塞调用线程。

带有双重检查锁定的私有静态实例

public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

内存高效+线程安全。

这样更有效,因为同步块只进入一次,而不是在访问已经创建的实例时。

另一种实现方法是使用Singleton Holder模式。

单例持有人模式

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
    private Singleton() {
    }
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

注意,SingletonHolder.instance,当它第一次被调用时,SingletonHolder类被加载。

然后,类装入器将创建静态单例实例并返回它。

这使得SingletonHolder类是线程安全的,因为它持有一个静态实例。

上述所有实现的一个共同缺陷是它们都不能避免序列化或反射。

有两种方法可以防止这种情况:

实现readResolve ()

public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    private Object readResolve() {
        return instance;
    }
}

通过实现readResolve()方法,该单例将被反序列化为已经创建的同一个实例。

枚举单例

public enum Singleton {
    INSTANCE;
    public void doSomething() {
    }
}

枚举只能有一定数量的实例/变体。这使得用枚举实现一个单例对象(只有一个可能的实例)成为可能。

枚举不受序列化和反射的影响。当一个enum被反序列化时,该实例将与已经存在的唯一可能的实例相同。

枚举实例化在设计上是线程安全的。

但是枚举单例也存在一定的问题,比如说内存效率问题。

哪个单例实现最适合你的用例?

  1. 如果内存不是问题,或者单例实例很轻,只需使用枚举单例。
  2. 如果内存是一个问题,使用惰性初始化单例。

    1. 此外,如果线程安全性存在问题,可以使用双重检查的锁单例或holder模式。
    2. 此外,如果你需要防止单例对象被序列化,可以使用readResolve()来双重检查锁定单例对象。
上一篇:Snap, AppImage和 Flatpak之间差异


下一篇:浅谈工厂模式