设计模式-单例模式

定义

保证一个类仅有一个实例,并提供一个全局访问点。
类型:创建型。

适用场景

  1. 想确保任何情况下都绝对只有一个实例。

优点

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

缺点

1.没有对外的接口,扩展困难。

重点

  1. 私有构造器,不能让外部new出该对象。
  2. 线程安全,防止在同一时刻多个线程进行对象的实例化。
  3. 延迟加载。
  4. 序列化和反序列化安全。
  5. 反射,防止反射攻击。

代码示例

懒汉模式

将类的实例化延后,在使用时进行初始化,但是此模式要注意线程安全的问题。
下面的代码,假如两个线程同时进行初始化的操作,就有可能出现线程安全问题。

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){
    }
    public static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
 }

上述代码进行演进使其线程安全,最简单的方式是在静态方法上加上synchronized,这种方式随便避免了线程安全的问题,但是在线程竞争比较激烈的情况,可能会引起线程堵塞饥饿等问题。

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){
    }
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
 }

DoubleCheck双重检查

重点

  1. 将synchronized关键字的位置放到了方法体内部,降低锁的粒度,增加程序的并发性。
  2. 第一重判断lazyDoubleCheckSingleton是否为null的意义在于,如果已经有线程完成了实例的初始化,那别的线程就没有必要再进行等待或者进入下一步了,直接可以拿到该实例进行返回。
  3. 加锁,同时刻只能有一个线程进行处理。
  4. 第二重判断,假如两个线程同时经过了第一重判断,但是在锁住的代码块中,只能有一个线程处理,另一个线程则等待获得monitor锁,直到第一个线程完成对象的初始化并释放锁,第二个线程进入锁的代码块,那第二重判断就起到了作用,此时无需在进行实例化,直接返回对象即可。
  5. 为什么要加volatile关键字,此处主要是在类的初始化过程中,看似是原子操作,实时并非如此,正常情况下它包含三个步骤:(1)分配内存给这个对象(2)初始化对象(3)设置lazyDoubleCheckSingleton 指向刚分配的内存地址。但是在实例化的过程中,JVM可能进行优化,实例化有可能的执行顺序变成了(1)(3)(2),那这样的话,在第一重的判断中,就会导致有的线程可能正在进行实例的初始化完成了(3),但是还没有完成(2),但是别的线程判断该对象已经分配了内存地址,会跳过if判断,直接返回该没有初始化完成的对象。
public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){

    }
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                    //1.分配内存给这个对象
//                  //3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
                    //2.初始化对象
//                    intra-thread semantics
//                    ---------------//3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

静态内部类-基于类初始化的延迟加载解决方案

类初始化的时机

  1. 类的实例被创建。
  2. 类中的静态方法被调用。
  3. 类中的静态成员被赋值。
  4. 类中的静态成员被使用,并且该成员不是常量成员。
  5. 该类是一个顶级类,并且类中有嵌套的断言语句。

初始化流程

在这里插入图片描述

  1. 当两个线程同时调用getInstance方法时,InnerClass的成员变量被使用,执行类的初始化。
  2. 类初始化过程中,JVM会首先加class对象的monitor锁,使得同一时刻只有一个线程能拿到monitor锁。
  3. 线程在创建StaticInnerClassSingleton实例的过程中,无论是否发生指令重排序,外部的线程是无法感知的,因为他们一直在堵塞等待。
  4. 当线程实例化对象完成之后,释放锁,其余线程拿到锁,获取成员变量就已经创建完成的了。
public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
    private StaticInnerClassSingleton(){
        
    }
}

饿汉式

  1. 使用static修饰的成员变量,并且在static代码块中完成对象的初始化,当类加载时,会执行static代码块完成对象的初始化。
  2. 此处可以使用final,也可不使用final,使用final修饰的成员变量必须要在类加载过程中,完成变量的初始化,并且不允许修改。
public class HungrySingleton {

    private final static HungrySingleton hungrySingleton;

    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

序列化破坏单例模式原理解析及解决方案

初始版本

以HungrySingleton为例,创建的单例对象,通过输出流将对象写到文件中,再从文件中读出该对象,两个对象不是同一个,也就破坏了单例。

public class HungrySingleton implements Serializable{

    private final static HungrySingleton hungrySingleton;

    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}
public class Test {
    public static void main(String[] args) throws Exception {

        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);

        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));

        HungrySingleton newInstance = (HungrySingleton) ois.readObject();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

执行结果如下
在这里插入图片描述

原理解析

那为什么会出现上述情况呢,通过查看ObjectInputStream的源码,readObject方法调用了readObject0
在这里插入图片描述
继续往下看,当是Object类型时,走圈出来的逻辑

在这里插入图片描述
readOrdinaryObject方法中,判断desc.isInstantiable(),如果是true,则调用反射生成一个新的对象。
在这里插入图片描述
该方法通过注释可以看出来,如果一个class是serializable/externalizable,并且可以通过序列化运行时实例化对象,则返回true,而我们的HungrySingleton实现了serializable,所以执行到此处时返回true,导致反射生成了新的对象。
在这里插入图片描述
readOrdinaryObject方法接着往下看,
在这里插入图片描述
上述分析之后,我们得知obj不是null,点进去desc.hasReadResolveMethod()方法,通过注释可以看出来,当类是serializable或者是externalizable类型,并且定义了readResolve方法,则返回true。
在这里插入图片描述
判断为true之后接着调用invokeReadResolve方法,调用该类的readResolve方法返回对象。
在这里插入图片描述

解决方案

通过上述分析,解决方案也基本清晰,那就是在单例的类中添加readResolve方法,该方法返回单例对象即可。

public class HungrySingleton implements Serializable{

    private final static HungrySingleton hungrySingleton;

    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }

    private Object readResolve(){
        return hungrySingleton;
    }
}

再次执行test方法,两个对象就是同一个对象了。
在这里插入图片描述

反射攻击解决方案及原理分析

基于类加载的单例初始化

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

        Class objectClass = HungrySingleton.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
        HungrySingleton instance = HungrySingleton.getInstance();
        
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}
public class HungrySingleton implements Serializable{

    private final static HungrySingleton hungrySingleton;

    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }

    private Object readResolve(){
        return hungrySingleton;
    }
}

执行main方法结果如下,很明显通过反射破坏了单例。
在这里插入图片描述
上述HungrySingleton中的单例是通过类加载的时候进行的初始化,静态内部类实例的初始化也是在类加载的时候进行的,所以静态内部类也有同样的问题,为了防止反射攻击,将两处的代码进行演进,当通过反射再次进行实例的创建时,判断hungrySingleton如果不为空,直接报错或者返回该实例。

public class HungrySingleton implements Serializable{

    private final static HungrySingleton hungrySingleton;

    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
        if(hungrySingleton != null){
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }

    private Object readResolve(){
        return hungrySingleton;
    }
}
public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
    private StaticInnerClassSingleton(){
        if(InnerClass.staticInnerClassSingleton != null){
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }
}

再次执行main方法,结果如下
在这里插入图片描述

懒汉式的单例初始化

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){
        if(lazySingleton != null){
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}
public class Test {
    public static void main(String[] args) throws Exception {

        Class objectClass = LazySingleton.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazySingleton instance = LazySingleton.getInstance();
        LazySingleton newInstance = (LazySingleton) constructor.newInstance();
        
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

执行main方法,相信大家已经能猜到结果了,直接抛出异常,因为我们在构造方法中,禁止了反射的攻击。
在这里插入图片描述
如果对上述main方法的顺序进行调整,改变这两行的顺序,LazySingleton instance = LazySingleton.getInstance();LazySingleton newInstance = (LazySingleton) constructor.newInstance();

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

        Class objectClass = LazySingleton.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        
        LazySingleton newInstance = (LazySingleton) constructor.newInstance();
        LazySingleton instance = LazySingleton.getInstance();
        
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

那执行结果会是啥样的呢?先进行了反射攻击,此时instance还没有赋值,于是会构造出对象实例,再次执行getInstance()方法,再次构造出实例对象,导致了会产生两个对象,显然也不是我们想要看到的。
在这里插入图片描述
总结:上述的懒汉模式无论怎么进行逻辑调整都抵挡不住反射的攻击,要想从根本上解决这个问题可以枚举创建的方式。

枚举单例

序列化破坏枚举单例

public enum EnumInstance {
    INSTANCE{
        protected  void printTest(){
            System.out.println("Galen Print Test");
        }
    };
    protected abstract void printTest();
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
    public static EnumInstance getInstance(){
        return INSTANCE;
    }

}

我们首先尝试通过序列化破坏枚举单例

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

        EnumInstance instance = EnumInstance.getInstance();
        instance.setData(new Object());
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);
        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        EnumInstance newInstance = (EnumInstance) ois.readObject();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
        
        System.out.println(instance.getData());
        System.out.println(newInstance.getData());
        System.out.println(instance.getData() == newInstance.getData());
    }
}

执行结果如下,仍然是同一个对象,且持有的data属性也是同一个对象。
在这里插入图片描述
那这是为什么呢?之前我们分析过ObjectOutputStream.readObject方法,当类型是枚举时,会调用readEnum方法。
在这里插入图片描述
在readEnum方法中,获取枚举对象的名字,然后通过Enum.valueOf获取枚举常量,因为枚举的名称都是唯一的,且都对应一个枚举常量,就都是同一个对象实例了。
在这里插入图片描述

反射破坏枚举单例

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

        EnumInstance instance = EnumInstance.getInstance();
        instance.setData(new Object());
        
        Class objectClass = EnumInstance.class;
        Constructor constructor = objectClass.getDeclaredConstructor();

        constructor.setAccessible(true);
        EnumInstance newInstance = (EnumInstance) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
        
        System.out.println(instance.getData());
        System.out.println(newInstance.getData());
        System.out.println(instance.getData() == newInstance.getData());
    }
}

执行结果直接抛出异常,没有获取到无参构造器。
在这里插入图片描述
下面分析一下Enum的源码,看一下为什么获取不到无参构造器呢,通过源码可以看出enum只有一个有参构造器。
在这里插入图片描述
那我们按enum的有参构造函数的格式,获取enum的有参构造器,再次执行。

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

        EnumInstance instance = EnumInstance.getInstance();
        instance.setData(new Object());
        
        Class objectClass = EnumInstance.class;
        Constructor constructor = objectClass.getDeclaredConstructor(String.class, int.class);

        constructor.setAccessible(true);
        EnumInstance newInstance = (EnumInstance) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
        
        System.out.println(instance.getData());
        System.out.println(newInstance.getData());
        System.out.println(instance.getData() == newInstance.getData());
    }
}

直接抛出异常,无法通过反射创建enum对象。
在这里插入图片描述
在这里插入图片描述

容器单例

此处容器用的hashMap,在多线程的情况下,可能会存在安全隐患,可以加锁,也可以选择一些线程安全的容器,此处讲述一下容器单例的思想,类似在Spring中的单例bean,就用到了容器单例。

public class ContainerSingleton {

    private ContainerSingleton(){

    }
    private static Map<String,Object> singletonMap = new HashMap<String,Object>();

    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);
    }
}

源码解析

JDK中的Runtime类

getRuntime方法运用了恶汉式单例模式,在类加载时就完成了对象的初始化。
在这里插入图片描述

JDK中的Desktop

源码中的context就是一个容器,是很明显的容器单例模式,并且运用了锁保证了操作容器的线程安全。
在这里插入图片描述

总结

单例模式是一个高频的面试题,看似是最简单的模式,其实有很多值得去挖掘的点,本文整理的单例模式的类型、优缺点、序列化的破坏、反射攻击、源码中的使用,希望大家能结合实践加深理解~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值