面试装X之--单例模式深度探索(玩转面试官)

之前详细总写过(单例模式)的己种写法,今天对这几种方法进行更深层次的总结

单例模式(Singleton Pattern)

是指一个类确保在任何情况下都只有一个实例,并提供一个全局访问点.
属于创建型模式

单例模式重点总结

1. 私有化构造器

指无法从自身之外的类来创建对象

2. 保证线程安全和延迟加载

饿汉模式: 在类初始化的时候就创建出类的实例(线程安全),但是同时由于对象不是即用既得,会带来内存上的开销
懒汉模式: 懒汉式避免了内存上的开销,但是同时带来了多线程的并发创建的问题,正所谓**鱼和熊掌不可兼得**

  • 再通过静态工厂方法获取对象的时候要保证对象不能被重复创建,可以对静态工厂方法加Synchronized锁
  • 为了避免锁的粒度太大,造成性能损耗.一般采用Double Check Lock[双重检查锁]的方式
 public  static lazyDoubleCheckSingleton getInstance() {
        if (lazy == null) {
            synchronized (lazyDoubleCheckSingleton.class) {
                if (lazy == null) {
                    lazy = new lazyDoubleCheckSingleton();
                }
            }
        }
        return lazy;
    }

上面的Synchronized总归是要带来性能损耗的, 有没有更好的方案呢?
我既要懒加载,又要没有性能损耗
**我全都要**

  • 利用静态内部类实现单例
public class LazyInnerClassSingleton {
//虽然构造方法私有,但是,逃不过反射
    private LazyInnerClassSingleton() {
        if(LazyHolder.LAZY != null){
            throw new RuntimeException("不允许构建多个实例");  // 可以防止通过反射来创建实例,破坏单例
        }
    }
//LzayHolder 里面的需要等到外部方法调用才执行
    // 巧妙的利用了内部类的特性
    // JVM底层执行逻辑,完美避免了线程安全问题
    public static final LazyInnerClassSingleton getInstance() {

        return LazyHolder.LAZY;
    }

    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();


    }
}

为什么主类中的静态块执行,而静态内部类中的静态方法不执行呢?
因为一旦程序运行,所有涉及的类[import导入的类和内部类]都会被加载到内存中,在整个程序的运行过程中类加载只会发生一次,如果这个类没有被加载到内存之中,那么之后就不会使用这个类.
只有类初始化之后,才会调用它的静态代码块,在这里我们只有对类进行引用的时候,才会进行初始化.
这里就是利用了内部类加载的特性,完美避免了线程安全问题

3. 防止反射破坏单例

public class LazyInnerClassSingletonTest {
    public static void main(String[] args) {
        Class<?> clazz = LazyInnerClassSingleton.class;
        try {
            Constructor c = clazz.getDeclaredConstructor(null);
            c.setAccessible(true);
            Object o1 = c.newInstance();  //强制创建对象
            Object o2 = LazyInnerClassSingleton.getInstance();
            System.out.println(o1 == o2);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

输出的结果是false ,
由于调用者开了上帝模式有了金手指,通过反射创建对象,但是我们可以给他搞个障眼法,让他不能简单的就能创建成功.

在构造方法加个限制,创建多个对象直接跑出异常

	 private LazyInnerClassSingleton() {
        if(LazyHolder.LAZY != null){
            throw new RuntimeException("不允许构建多个实例");  // 可以防止通过反射来创建实例,破坏单例
        }
    }

4. 防止序列化破坏单例

当我们将一个对象创建好,有时候需要将对象序列化之后存入磁盘,下次使用时候再从磁盘中读取对象,反序列化为内存对象.
反序列化后的对象会重新分配内存.如果序列化的对象为单例对象,就违背了单例对象的初衷,相当于破坏了单例.
public class SeriableSingleton implements Serializable {
    //序列化就是说把内存中的状态通过转换成字节码的形式
//从而转换一个 IO 流,写入到其他地方(可以是磁盘、网络 IO)
//内存中状态给永久保存下来了
//反序列化
//讲已经持久化的字节码内容,转换为 IO 流
//通过 IO 流的读取,进而将读取的内容转换为 Java 对象
//在转换过程中会重新创建对象 new
    public final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}
    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }
}
  public static void main(String[] args) {
        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();
        FileOutputStream fos = null; // 输出流
        try {
            //将对象序列化并写入到文件中
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton) ois.readObject();
            ois.close();
            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);


        } catch (Exception e) {
            e.printStackTrace();
        }
    }

结果: 在这里插入图片描述
很明显单例被破坏

问题来了,序列化怎么破坏单例的?如何避免这种破坏?

接下来就是本文的重头戏了

深入源码抽丝剥茧

  1. 从这开始读取对象
    在这里插入图片描述
    在这里插入图片描述
    综上所述 :
    我们只能重写一个叫做ReadREsolve()的方法,并返回单例对象来避免单例被破坏.
//重写readResolve方法,只不过覆盖了反序列化出来的对象
//还是创建了两次,发生在JVM层面,相对来说比较安全
//之前反序列化出来的对象会被GC回收	
 public SeriableSingleton ReadResolve(){
        return INSTANCE;
    }

注册式单例

注册式单例 : 就是将每一个实例都登记到某一个地方,使用唯一的标识获取单例.
注册式单例有两种写法:

  • 一种为容器式缓存
  • 一种为枚举登记

枚举式单例

public enum EnumSingleton {
    INSTANCE ;
    private  Object data ;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumSingleton getInstance(){
        return  INSTANCE ;
    }
}

测试反序列化破坏单例
在这里插入图片描述
使用jad反编译工具 :
在这里插入图片描述
利用了static实例化对象,没有线程安全问题

问题又来了,它是怎么避免反射破坏单例的呢?反序列化破坏单例的呢?

序列化

在这里插入图片描述

反射

在这里插入图片描述
深入源码分析

在这里插入图片描述
从JDK层面就为Enum不被序列化和反射破坏单例保驾护航

容器式单例

  • 便于管理对象,同时也是懒加载
  • 存在线程安全问题
ublic class ContainerSingleton {
    private ContainerSingleton() {
    }

    private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getBean(String className) {
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                return obj;
            }

            return ioc.get(className);
        }
    }
}

到此为止了吗?

还有ThreadLocal实现的单例(以后再说)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值