单例模式用于确保一个类只有一个实例,并提供一个全局访问点。
一般使用场景
-
日志:单例日志记录器用于将消息记录到文件中。
-
数据库连接:单例数据库连接用于连接到数据库。
-
配置:单例配置对象用于存储应用程序配置。
-
缓存:单例缓存用于存储应用程序数据。
如何实现单例模式
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() {
}
}
这意味着在加载类时创建一个静态单例实例。
这是最常见的实现,但它的内存效率不高。如果不使用类,单例仍然会被创建,如果单例很重,它将消耗大量内存。
让我们来看看创建单例时的一些重要问题。
-
线程安全:因为单例是为多个线程创建的,所以它们需要线程安全。线程安全可以从两方面来看待。
-
确保实例不会被创建吵过一次
-
如果单例对象保存数据,就像在缓存中一样,确保数据是线程安全的。更新数据的方法应该同步。
-
-
效率:我们需要确保最佳的内存使用和性能
-
内存泄漏:如果单例很重且不使用,它将消耗大量内存
-
资源使用:如果单例模式消耗系统资源,它将消耗大量的CPU周期。在这种情况下,除非使用单例,否则系统将得不到充分利用。
-
序列化和反序列化:如果单例被序列化和反序列化,它将被重新创建,在这种情况下将创建多个实例。然而,序列化单例并不常见,理解这一点很重要
-
所以让我们看看上述的单例实现是否考虑到了这些问题。
-
在实例创建方面是线程安全的——是的,因为在加载类时只创建一次静态实例。
-
内存使用不是最优的,因为单例是在加载类时创建的,而不是在使用类时创建的。
-
不会阻止单例对象被序列化和反序列化。
现在我们来逐个看看其他的方法。
私有静态实例与惰性初始化
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被反序列化时,该实例将与已经存在的唯一可能的实例相同。
枚举实例化在设计上是线程安全的。
但是枚举单例也存在一定的问题,比如说内存效率问题。
哪个单例实现最适合你的用例?
-
如果内存不是问题,或者单例实例很轻,只需使用枚举单例。
-
如果内存是一个问题,使用惰性初始化单例。
-
此外,如果线程安全性存在问题,可以使用双重检查的锁单例或holder模式。
-
此外,如果你需要防止单例对象被序列化,可以使用readResolve()来双重检查锁定单例对象。
-
本文为从大数据到人工智能博主「xiaozhch5」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://lrting.top/backend/2566/