设计模式笔记(三)—— 单例模式

设计模式学习笔记,感谢geely老师的《Java设计模式精讲 Debug方式+内存分析》课程。

单例模式(Singleton Pattern)

定义:

保证一个类仅有一个实例,并提供一个全局访问点

类型:

创建型

适用场景:

想确保任何情况下都绝对只有一个实例
ServletContext单实例多线程,ServletConfig,ApplicationContext,DBPool

优点:

在内存里只有一个实例,减少了内存开销;可避免对资源的多重占用;设置全局访问点,严格控制访问

缺点:

没有接口,扩展困难

重点:
  • 私有构造器
  • 线程安全
  • 延迟加载
  • 序列化和反序列化安全
  • 反射

实现:

懒汉式:

指令重排序问题
在这里插入图片描述
1、禁止重排序

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton instance = null;
    private LazyDoubleCheckSingleton() {

    }
    public static LazyDoubleCheckSingleton getInstance() {
        if (instance == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (instance == null) {
                    instance = new LazyDoubleCheckSingleton();
                }
            }
            instance = new LazyDoubleCheckSingleton();
        }
        return instance;
    }
}

2、基于类初始化的延迟加载
在这里插入图片描述

public class StaticInnerClassSingleton {
    private static class InnerClass {
        private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.instance;
    }
}
    public StaticInnerClassSingleton() {
    }

饿汉式:

public class HungrySingleton {
    private final static HungrySingleton instance;
    static {
        instance = new HungrySingleton();
    }
    private HungrySingleton() {

    }
    public static HungrySingleton getInstance() {
        return instance;
    }
}

序列化破坏单例模式

在这里插入图片描述
因为实现了序列化接口,isInstantiable()返回true,所以在序列化时会反射创建新的实例,破坏了单例模式。

解决:

需要添加一个readResolve()方法

    private Object readResolve() {
        return instance;
    }

如果含有readResolve方法,则会通过反射调用该方法。
因此,根据添加的方法,返回的是已经创建过的同一实例
在这里插入图片描述
在这里插入图片描述

反射攻击

虽然空参构造函是private的,但可以设置构造器的权限,就可以反射调用构造器创建实例,于是就会产生不同的实例。

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class hungrySingletonClass = HungrySingleton.class;
        Constructor declaredConstructor = hungrySingletonClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        HungrySingleton instanceByCons = (HungrySingleton) declaredConstructor.newInstance();
        HungrySingleton instance = HungrySingleton.getInstance();
        System.out.println(instanceByCons == instance);
    }
解决:

饿汉式与静态内部类的懒汉式可使用该方案,因为在类加载时就实例化了对象,因此instance不为null,反射调用构造器时就会抛出异常

    private HungrySingleton() {
        if (instance != null) {
            throw new RuntimeException("单例构造器静止反射调用");
        }
    }

而不是在类加载就创建实例的懒汉式的不能防御反射攻击

但是有一种单例既可以防止序列化破坏,又可以防御反射攻击,那就是枚举单例

枚举单例

public enum EnumInstance {
    INSTANCE;

    @Getter
    @Setter
    private Object data;

    public static EnumInstance getInstance() {
        return INSTANCE;
    }
}

序列化
测试返回true

    public static void main(String[] args) throws Exception {
        EnumInstance instance = EnumInstance.getInstance();
        instance.setData(new Object());

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
        oos.writeObject(instance);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("singleton")));
        EnumInstance enumInstance = (EnumInstance) ois.readObject();
        System.out.println(instance.getData() == enumInstance.getData());
    }

ObjectInputStream中readEnum()方法
在这里插入图片描述
readString()获取到枚举对象的名称name
通过Class类型和name获取枚举常量,这个枚举常量是唯一的,就能保证单例

反射

   public static void main(String[] args) throws Exception {
        Class instanceClass = EnumInstance.class;
        Constructor constructor = instanceClass.getDeclaredConstructor();
        constructor.setAccessible(true);
   }

报错NoSuchMethodException,是getDeclaredConstructor()那一行
获取构造器时没有获得无参构造器
在这里插入图片描述
查看Enum类,发现它只有一个有参的构造方法
在这里插入图片描述
那我们根据有参构造进行获取构造器

    public static void main(String[] args) throws Exception {

        Class instanceClass = EnumInstance.class;
        Constructor constructor = instanceClass.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        EnumInstance instance = (EnumInstance) constructor.newInstance("Orcas", 11);
    }

这个时候报错的就是创建实例那一行
不能反射创建枚举对象
在这里插入图片描述
我们可以在Constructor类中看到,在构建实例时,它会进行判断是否是枚举类,如果是,它就会抛出刚才的异常
在这里插入图片描述

由此可见,枚举类可以通过ObjectInputStream和Constructor类中相应的逻辑来避免序列化和反射对单例的破坏。

而枚举类本身我们通过反编译可以看到
它拥有私有构造器,在加载时初始化完成了INSTANCE,类似饿汉模式

// 枚举类被final修饰
public final class EnumInstance extends Enum {
    ...
    ...
    // 私有构造器
	private EnumInstance(String s, int i)
    {
        super(s, i);
    }
    public static EnumInstance getInstance()
    {
        return INSTANCE;
    }
	// 静态
    public static final EnumInstance INSTANCE;
    private Object data;
    private static final EnumInstance $VALUES[];
    // 通过静态代码块来实例化INSTANCE
    static 
    {
        INSTANCE = new EnumInstance("INSTANCE", 0);
        $VALUES = (new EnumInstance[] {
            INSTANCE
        });
    }
}

容器单例

线程不安全

public class ContainerSingleton {
	private ContainerSingleton() {
	}
    private static Map<String, Object> singletonMap = new HashMap<>();
    public static void putInstance(String key, Object instance) {
        if (StringUtils.isNotBlank(key) && instance != null) {
            if (!singletonMap.containsKey(key)) {
                singletonMap.put(key, instance);
            }
        }
    }
    public static Object getInstance(String key) {
        return singletonMap.get(key);
    }
}

ThreadLocal线程单例

同一个线程中单例

public class ThreadLocalInstance {
    private static final ThreadLocal<ThreadLocalInstance> threadLocalInstance = new ThreadLocal<ThreadLocalInstance>() {
        @Override
        protected ThreadLocalInstance initialValue() {
            return new ThreadLocalInstance();
        }
    };
    private ThreadLocalInstance() {

    }
    public static ThreadLocalInstance getInstance() {
        return threadLocalInstance.get();
    }
}

单例模式的应用

1、Runtime
饿汉式
在这里插入图片描述
2、Desktop
类似容器单例
在这里插入图片描述
3、AbstractFactoryBean
在这里插入图片描述
4、Mybatis中ErrorContext
基于ThreadLocal的线程单例
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值