一.介绍
单例模式(Singleton Pattern)属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象(不需要手动去new)。本文会结合源码加深你对单例模式的理解
二.实现方式
- 大体上分为四种实现方式:饿汉式、懒汉式、静态内部类、枚举类
- 实现思想基本一致:
- 提供一个唯一的私有实例
- 私有化构造器
- 提供一个公共方法获取私有实例
- 饿汉式
/**
* 饿汉式
*/
public class Demo1 {
private static Demo1 instance = new Demo1();
private Demo1(){
}
public static Demo1 getInstance(){
return instance;
}
}
在JVM里面这个类只会被实例化一次,实例化的过程由JVM保证线程的安全(JVM以同步的形式来完成类加载的整个过程)
4. 懒汉式
/**
* 懒汉式(双重检测锁)
*/
public class Demo2 {
/**
* volatile禁止指令重排
*/
private volatile static Demo2 instance;
private Demo2() {
}
public static Demo2 getInstance() {
//防止instance不为null的情况下,某一线程持有锁时间过长,导致其他线程长时间等待
if (instance == null) {
//synchronized关键字确保线程安全
synchronized (Demo2.class) {
if (instance == null) {
instance = new Demo2();
}
}
}
return instance;
}
}
- 静态内部类
/**
* 静态内部类
*/
public class Demo3 {
private Demo3(){
}
private static class Inner{
//将[提供一个唯一的私有实例的操作]放在了静态内部类
private static Demo3 instance = new Demo3();
}
public static Demo3 getInstance(){
return Inner.instance;
}
}
- 枚举
/**
* 枚举类
*/
public enum Demo4 {
INSTANCE;
}
我们可以通过反编译Demo4的class文件看出,这种方式也无非就是提供了一个唯一的静态实例
三.单例模式的破坏与预防
- 序列化破坏单例
/**
* 序列化破坏单例模式
*/
public class SerializeAttack {
public static void main(String[] args) throws Exception {
Demo1 instance = Demo1.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("instance"));
oos.writeObject(instance);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("instance"));
Demo1 instance1 = (Demo1) ois.readObject();
ois.close();
//反序列化获取的对象与序列化前的对象不相同
System.out.println(instance == instance1); //false
}
}
- 序列化预防(枚举类天生具有)
Serializable接口的源码中已经给出了解决方案-提供readResolve方法
具体实现
Object readResolve() throws ObjectStreamException{
return instance;
}
为什么提供了readResolve方法就能预防序列化破坏?我们看一下readObject方法的源码
为什么枚举类天生预防反序列化?我们再看一下readObject方法的源码
- 反射破坏单例
/**
* 反射破坏单例模式
*/
public class ReflectAttack {
public static void main(String[] args) throws Exception {
Constructor<Demo1> constructor = Demo1.class.getDeclaredConstructor();
constructor.setAccessible(true);
System.out.println(constructor.newInstance() == Demo1.getInstance());
}
}
- 反射预防(枚举类天生具有,懒汉式无法预防)
具体实现
//修改私有构造器的逻辑
private Demo1(){
if(instance != null){
throw new RuntimeException("prevent reflectAttack");
}
}
为什么枚举类天生预防反射攻击?我们来看下newInstance方法的源码
四.在JDK中的应用
单例模式在JDK中最经典的应用莫过于RunTime类(饿汉式)
五.在Spring中的应用
org.springframework.core.ReactiveAdapterRegistry(懒汉式)
六.饿汉模式的缺点(拓展)
饿汉模式无法控制只有在调用getIntance方法的时候才进行初始化(无法进行懒加载)
1.我们先来看下类加载过程
- 加载二进制数据到内存,生成对应的Class数据结构
- 连接:a.验证,b.准备(给类的静态成员变量赋默认值),c.解析
- 初始化:给类的静态变量赋初值
2.什么时候会触发初始化?
当前类是启动类即main函数所在类、直接进行new操作、访问静态属性、访问静态方法、用反射访问类、初始化一个类的子类等
3.饿汉模式无法控制只有在调用getIntance方法的时候才进行初始化
这里采用访问静态属性的方式举例,并给饿汉式提供一个用于验证的静态变量name
SingleTon.java
/**
1. 饿汉式
*/
public class SingleTon{
//静态属性name,仅用于验证
public static String name;
private static SingleTon instance = new SingleTon();
private SingleTon(){
//控制台打印,仅用于验证
System.out.println("初始化了");
}
public static SingleTon getInstance(){
return instance;
}
}
测试类SingletonTest.java
public class SingletonTest {
public static void main(String[] args) {
System.out.println(Demo1.name); //初始化了 null
}
}
可以看出在未调用getIntance方法的时候,intance实例被初始化了
4.如何弥补这个缺点?
采用静态内部类的方式可以弥补这个缺点,我们来验证一下
SingleTon.java
/**
* 静态内部类
*/
public class SingleTon{
//静态属性name,仅用于验证
public static String name;
private SingleTon(){
//控制台打印,仅用于验证
System.out.println("初始化");
}
private static class Inner{
private static SingleTon instance = new SingleTon();
}
public static SingleTon getInstance(){
return Inner.instance;
}
}
测试类SingletonTest.java
public class SingletonTest {
public static void main(String[] args) {
System.out.println(SingleTon.name); //null
}
}
可以看出在访问外部静态变量的时候,instance实例没有被初始化