这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战
概述
单例模式(Singleton Pattern)是一种对象创建型模式,它确保了某个类只有一个实例,并且自行实例化来供外部使用。为了防止在外部对其实例化,会将其构造函数设计为私有。单例模式分为两大类,分别是饿汉式单例和懒汉式单例。
饿汉式
饿汉式单例类是实现起来最简单的单例类,由于在定义静态变量的时候实例化了单例类,因此在类加载的时候就会创建该单例对象,饿汉式的”饿“体现在这里。其示例如下:
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() { }
public static EagerSingleton getInstance() {
return instance;
}
}
当 EagerSingleton 类被加载时,静态变量 instance 会被初始化,此时该类的私有构造函数会被调用,从而创建该类的唯一实例。
- 优点:写法简单;类加载就会实例化,程序性能高。
- 缺点:无论实例是否被使用都会被创建,因此会导致内存的浪费。
懒汉式
懒汉式单例在类加载时并不自行实例化,它在调用 getInstance() 方法时,先判断单例类是否实例化,若没有实例化才被创建。,为了避免多个线程同时调用 getInstance() 方法,使用了 synchronized 关键字,从而保证该单例类的实例唯一。
实例在被需要时才被创建,这种技术称为延迟加载(Lazy Load),也是因为这种“火烧眉毛”的方式,所以被称为懒汉式。
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() { }
synchronized public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
懒汉式的改进
懒汉式单例为了保证单例类在多线程中的唯一性,在 getInstance() 方法前面增加了 synchronized 关键字。虽然这种方式解决了线程安全问题,但是每次调用 getInstance() 时都需要进行线程锁定判断,在高并发访问环境中,会导致系统性能大大降低。为了解决这个问题,需要对懒汉式进行改进,我们无须对整个 getInstance() 方法进行锁定,只需对创建实例的代码进行锁定即可。 getInstance() 方法改进如下:
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
return instance;
}
懒汉式进一步的改进
使用以上代码来实现单例,还是会存在单例对象的不唯一。假如在某个瞬间线程 A 和线程 B 同时调用 getInstance(),此时 instance 对象为 null,都能通过 instance == null 的判断。由于对单例类的加锁,线程 A 进入 synchronized 锁定的代码中执行实例的创建,线程 B 处于排队等待状态。但当 A 执行完毕时,线程 B 并不知道实例已经创建,将继续创建新的实例,因此会创建多个单例对象,这就违背了单例模式的设计思想,因此需要进行进一步改进,在 synchronized 代码中再进行一次 instance == null 判断,这种方式称为双重检查锁定(DCL,Double-Check Locking)。其代码所示如下:
public class LazySingleton {
private volatile static LazySingleton instance = null;
private LazySingleton() { }
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
注意:使用双重检查锁定来实现懒汉式单例,需要对静态成员变量 instance 增加 volatile 修饰符,以此来保证多个线程之间的可见性,防止指令重排,确保实例创建的唯一性。由于 volatile 关键字会屏蔽 JVM 所做的一些优化,可能会导致系统运行效率降低。
懒汉式的更好实现
上述代码虽然保证了在多线程环境中单例类的唯一,但是会导致性能的下降。在 Java 中我们可以通过静态内部类来创建单例对象,再将该单例对象通过getInstance() 方法返回给外部使用,这种技术称为IoDH(Initialization Demand Holder),其实现如下:
public class LazySingleton {
private LazySingleton() {}
private static class LazyInstanceClass {
private final static Singleton instance = new Singleton();
}
public static LazySingleton getInstance() {
return LazyInstanceClass.instance;
}
}
由于静态单例对象 instance 不是单例类 LazySingleton 的成员变量,因此在单例类加载时,不会被实例化。只有在第一次调用 getInstance() 方法时,才会通过静态内部类 LazyInstanceClass 初始化该类下的静态成员变量 instance,由 JVM 来保证其线程的安全性,确保该成员变量只初始化一次。
懒汉式的再完善
通过 IoDH,既实现了延迟加载,又保证了线程的安全,但仍然可以通过反射攻击或者反序列化攻击打破单例类的唯一性。
- 反射攻击:
Singleton singleton = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton newSingleton = constructor.newInstance();
- 反序列化攻击:
Singleton instance = Singleton.getInstance();
byte[] serialize = SerializationUtils.serialize(instance);
Singleton newInstance = SerializationUtils.deserialize(serialize);
枚举可以解决这些问题,最佳的单例实现模式就是枚举模式。利用枚举的特性,可以让 JVM 来保证线程的安全和单一实例的问题。其实现如下:
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
优点
- 内存中始终只有一个实例,节省了内存,避免了对资源的多重占用。
- 提升系统性能,当一个对象需要的资源较多时,可以初始化一个实例,然后永久驻留在内容中,如读取配置。
缺点
单例模式一般没有接口,不利于扩展。
应用场景
-
在整个项目中需要一个共享访问点或共享数据。
-
创建一个对象所消耗的资源过多,如访问IO和数据库等资源。
-
需要定义大量的静态常量和静态方法的环境,如工具类。
JDK 中的应用
JDK 中的 java.lang.Runtime 就是单例模式中的饿汉式。
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}