单例模式
- [单例模式 (Singleton) (重点)](#单例模式 (Singleton) (重点))
-
- 1) 为什么要使用单例 为什么要使用单例)
- 2) 如何实现一个单例 如何实现一个单例)
-
- 2.a) 饿汉式 饿汉式)
- 2.b) 懒汉式 懒汉式)
- 2.c) 双重检查锁 双重检查锁)
- 2.d) 静态内部类 静态内部类)
- 2.e) 枚举类 枚举类)
- 2.f) 反射入侵 反射入侵)
- 2.g) 序列化与反序列化安全 序列化与反序列化安全)
- 3) 单例存在的问题 单例存在的问题)
-
- 3.a) 无法支持面向对象编程 无法支持面向对象编程)
单例模式 (Singleton) (重点)
一个类只允许创建一个对象(或者实例),那这个类就是一个单例类
1) 为什么要使用单例
1.表示全局唯一
如果有些数据在系统中应该且只能保存一份,那就应该设计为单例类:
- 配置类:在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,应该被映射为一个唯一的【配置实例】
- 全局计数器:我们使用一个全局的计数器进行数据统计、生成全局递增ID等功能。若计数器不唯一,很有可能产生统计无效,ID重复等
2.处理资源访问冲突
如果使用单个实例输出日志,锁【this】即可。
如果要保证JVM级别防止日志文件访问冲突,锁【class】即可。
如果要保证集群服务级别的防止日志文件访问冲突,加分布式锁即可
2) 如何实现一个单例
常见的单例设计模式,有如下五种写法,在编写单例代码的时候要注意以下几点:
- 1.构造器需要私有化
- 2.暴露一个公共的获取单例对象的接口
- 3.是否支持懒加载(延迟加载)
- 4.是否线程安全
2.a) 饿汉式
在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的
java
/**
* 饿汉式单例的实现
* - 不支持懒加载
* - jvm保证线程安全
*/
public class EagerSingleton {
/**
* 当启动程序的时候,就创建这个实例
*/
// 1.持有一个jvm全局唯一的实例
private static final EagerSingleton instance = new EagerSingleton();
// 2.为了避免别人随意的创建,需要私有化构造器
private EagerSingleton() {
}
// 3.暴露一个方法,用来获取实例
public static EagerSingleton getInstance() {
return instance;
}
}
2.b) 懒汉式
懒汉式相对于饿汉式的优势是支持延迟加载,具体的代码实现如下所示:
支持延迟加载
java
/**
* 懒汉式单例的实现
* - 支持懒加载
*/
public class LazySingleton {
/**
* 当需要使用这个实例的时候,再创建这个实例
*/
// 1.持有一个jvm全局唯一的实例
private static LazySingleton instance;
// 2.为了避免别人随意的创建,需要私有化构造器
private LazySingleton() {
}
// 3.暴露一个方法,用来获取实例
// - 懒加载-线程不安全,因为当面对大量并发请求时,有可能会有超过一个线程同时执行此方法,是无法保证其单例的特点
// - 加锁:使用 synchronized,(对.class加锁) 但是方法上加锁会极大的降低获取单例对象的并发度
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
2.c) 双重检查锁
饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测锁:
java
/**
* 双重检查锁单例的实现
*/
public class DoubleCheckLockSingleton {
// 1.持有一个jvm全局唯一的实例
// - 因为创建对象不是一个原子性操作,即使使用双重检查锁,也可能在创建过程中产生半初始化状态
// - volatile 1.保证内存可见 2.保存有序性
// - jdk1.9以上,不加volatile也可以,jvm内部处理有序性
private static volatile DoubleCheckLockSingleton instance;
// 2.为了避免别人随意的创建,需要私有化构造器
private DoubleCheckLockSingleton() {
}
// 3.暴露一个方法,用来获取实例
// - 第一次创建需要上锁,一旦创建完成,就不再需要上锁
// - 事实上获取单例并没有线程安全的问题
public static DoubleCheckLockSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckLockSingleton.class) {
// 创建
if (instance == null) {
instance = new DoubleCheckLockSingleton();
}
}
}
return instance;
}
}
2.d) 静态内部类
比双重检测更加简单的实现方法,那就是利用 Java 的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。
当外部类 InnerSingleton()被加载的时候,并不会创建 InnerSingleton的实例对象。只有当调用 getInstance() 方法时,InnerSingletonHolder 才会被加载,这个时候才会创建 instance实例。
java
/**
* 静态内部类的方式实现单例
*/
public class InnerSingleton {
// 1.私有化构造器
private InnerSingleton() {
}
// 2.提供一个方法,获取单例对象
public InnerSingleton getInstance() {
return InnerSingletonHolder.instance;
}
// 3.定义内部类,来持有实例
// - 特性:类加载的时机 --> 一个类会在第一次使用的时候被加载
// - 实例会在内部类加载(调用getInstance()方法之后)会创建
private static class InnerSingletonHolder {
private static final InnerSingleton instance = new InnerSingleton();
}
}
2.e) 枚举类
基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。
java
/**
* 枚举:累加器
*/
public enum GlobalCounter {
// 这个INSTANCE是一个单例
// 对于枚举类。任何一个枚举项就是一个单例
// 本质上就是 static final GlobalCounter instance = new GlobalCounter()
INSTANCE;
private AtomicLong atomicLong = new AtomicLong(0);
public Long getNumber() {
return atomicLong.getAndIncrement();
}
}
2.f) 反射入侵
事实上,我们想要阻止其他人构造实例 仅仅私有化构造器还是不够的,因为我们还可以使用反射获取私有构造器进行构造,当然使用枚举的方式是可以解决这个问题的,对于其他的书写方案,我们通过下边的方式解决:
java
// 反射代码
Class<DoubleCheckLockSingleton> instance = DoubleCheckLockSingleton.class;
Constructor<DoubleCheckLockSingleton> constructor = instance.getDeclaredConstructor();
constructor.setAccessible(true);
boolean flag = DoubleCheckLockSingleton.getInstance() == constructor.newInstance();
log.info("flag -> {}",flag);
java
/**
* 单例的防止反射入侵的代码实现
*/
public class ReflectSingleton {
/**
* 可以使用反射获取私有构造器进行构造
*/
private static volatile ReflectSingleton instance;
// 为了避免别人随意的创建,需要私有化构造器
private ReflectSingleton() {
// 升级版本 --> 不要让人使用反射创建
if (instance != null) {
throw new RuntimeException("该对象是单例,无法创建多个");
}
}
public static ReflectSingleton getInstance() {
if (instance == null) {
synchronized (ReflectSingleton.class) {
// 创建
if (instance == null) {
instance = new ReflectSingleton();
}
}
}
return instance;
}
}
2.g) 序列化与反序列化安全
事实上,到目前为止,我们的单例依然是有漏洞的
java
/**
* 通过序列化
*/
@Test
public void testSerialize() throws Exception {
// 获取单例并序列化
SerializableSingleton instance = SerializableSingleton.getInstance();
FileOutputStream fout = new FileOutputStream("F://singleton.txt");
ObjectOutputStream out = new ObjectOutputStream(fout);
out.writeObject(instance);
// 将实例反序列化出来
FileInputStream fin = new FileInputStream("F://singleton.txt");
ObjectInputStream in = new ObjectInputStream(fin);
Object o = in.readObject();
log.info("是同一个实例吗 {}", o == instance); // 是同一个实例吗 false
}
在进行反序列化时,会尝试执行readResolve方法,并将返回值作为反序列化的结果,而不会克隆一个新的实例,保证jvm中仅仅有一个实例存在
java
public class Singleton implements Serializable {
// 省略其他的内容
public static Singleton getInstance() {
}
// 需要加这么一个方法
public Object readResolve(){
return singleton;
}
}
3) 单例存在的问题
在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID 生成器类。单例模式书写简洁、使用方便,在代码中,我们不需要创建对象。但是,这种使用方法有点类似硬编码(hard code),会带来诸多问题,所以我们一般会使用spring的单例容器作为替代方案。
3.a) 无法支持面向对象编程
OOP 的三大特性是封装、继承、多态 。单例将构造私有化,直接导致的结果就是,他无法成为其他类的父类,这就相当于直接放弃了继承和多态的特性,也就相当于损失了可以应对未来需求变化的扩展性,以后一旦有扩展需求,比如写一个类似的具有绝大部分相同功能的单例,我们不得不新建一个十分【雷同】的单例。