创建型模式--单例模式
简介:单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象(例如数据库连接池)。
单例模式有两种类型:
- 懒汉式:在真正需要使用对象时才去创建该单例类对象。
- 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用。
懒汉式创建单例对象
懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象。,否则则先执行实例化操作。因为需要两次判空,且对类对象加锁,该懒汉式写法也被称为:Double Check(双重校验) + Lock(加锁)。
public class LazySingleton {
private static volatile LazySingleton instance; // 使用volatile关键字确保多线程环境下的可见性
private LazySingleton() {
// 私有构造函数,避免外部类直接实例化
}
public static LazySingleton getInstance() {
if (instance == null) { // 第一次判空
synchronized (LazySingleton.class) { // 加锁
if (instance == null) { // 第二次判空
instance = new LazySingleton(); // 实例化操作
}
}
}
return instance;
}
}
饿汉式创建单例对象
饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。
关于类加载,我们目前可以简单认为在程序启动时,这个单例对象就已经创建好了。
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton(); // 在类加载时就创建实例
private EagerSingleton() {
// 私有构造函数,避免外部类直接实例化
}
public static EagerSingleton getInstance() {
return instance;
}
}
破坏懒汉式单例与饿汉式单例
无论是完美的懒汉式还是饿汉式,终究敌不过反射和序列化,它们俩都可以把单例对象破坏掉(产生多个对象)。
枚举实现单例(Java)
使用枚举实现单例模式的优势在哪里?
- 优势1:代码对比饿汉式与懒汉式来说,更加地简洁。
- 优势2:它不需要做任何额外的操作去保证对象单一性与线程安全性。
- 优势3:使用枚举可以防止调用者使用反射、序列化与反序列化机制强制生成多个单例对象,破坏单例模式。
- 防反射
枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。 - 防止反序列化创建多个枚举对象
在读入 Singleton 对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,利用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。Java会根据枚举实例的名称来匹配对应的枚举实例。
所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。
- 防反射
小总结:
- Enum 类内部使用 Enum 类型判定防止通过反射创建多个对象
- Enum 类通过写出(读入)对象类型和枚举名字将对象序列化(反序列化),通过 valueOf() 方法匹配枚举名找到内存中的唯一的对象实例,防止通过反序列化构造多个对象
- 枚举类不需要关注线程安全、破坏单例和性能问题,因为其创建对象的时机与饿汉式单例有异曲同工之妙。
总结
- 单例模式常见的写法有两种:懒汉式、饿汉式。
- 懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题。
- 饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。
- 在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象。
- 如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题。
- 为了防止多线程环境下,因为指令重排序导致变量报 NPE,需要在单例对象上添加 volatile 关键字防止指令重排序
- 最优雅的实现方式是使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例(Java)。