免费网络教学平台,青岛济南网站建设优化,即墨公司做网站,杭州江干区抖音seo哪里有为什么要问这个问题#xff1f;
我们知道#xff0c;单例是一种很常用的设计模式#xff0c;主要作用就是节省系统资源#xff0c;让对象在服务器中只有一份。但是实际开发中可能有很多人压根没有写过单例这种模式#xff0c;只是看过或者为了面试去写写demo熟悉一下。那…为什么要问这个问题
我们知道单例是一种很常用的设计模式主要作用就是节省系统资源让对象在服务器中只有一份。但是实际开发中可能有很多人压根没有写过单例这种模式只是看过或者为了面试去写写demo熟悉一下。那为啥说是一种常用的模式 其实我们用的spring管理对象生命周期用到默认的scope就是单例。这样的场景几乎每天都在用所以我们不需要自己手写单例了。 那么为了面试进大厂是不是就要刷刷文章学习学习呢当我们刷完单例的整体结构时会发现还是很简单的嘛无非就是懒汉、饿汉。饿汉上来就创建没什么难的懒汉可能会在创建的时候线程不安全还要防止jvm在server模式下进行指令重排加双层锁判断就ok啦面试很简单嘛照着文章中的背下来就行了。 但是你有没有遇到面试官问你如何构造一个安全的单例注意是安全的。 如果你没遇到恭喜你面试官不想为难你或者他没把单例玩明白。如果你遇到了很不幸这个面试官是个注重细节的人而且在给你挖坑。 当然我觉得在面试的时候这么问单例的人可能只有我。 刚刚说了线程不安全加锁就解决了。但是这里安全可不只是是线程安全光知道线程安全没什么特殊的无非你准备过面试。那这里还有什么猫腻 答案
反射攻击导致单例变成多例不安全了序列化、反序列化变成多例不安全了
这两点下面再说具体为什么和怎么解决。先说说作为面试官的我为什么这么问 首先我知道现在存在很多刷题网站说用过的人并不一定真的用过只是刷了题我要筛出真正会的人而不是刷过题的人。 其次一个单例考察的不是一个模式这么简单如果回答出这两个答案的人我会认为他的java基础非常好而且考虑问题非常全面和谨慎。怎么看出来的 基础好我们最常用的序列化方式恐怕就是json、xml、pb、hessian等协议很少有人用java自带的字节流序列化。用字节流序列化只有一种情况redis存储、消息报文投递、IO编程时考虑性能还有可能对字节进行压缩。这个时候如果你只是对Serializable接口有所了解知道serialVersionUID就有一些浅了。如果你知道readResolve那证明你对java序列化理解的很透彻。 考虑问题全面和谨慎一般的人实现单例只是满足功能就可以了甚至不考虑懒汉的线程安全问题。如果你考虑反射攻击带来的危害那你在做架构方案设计时一定是很全面和谨慎的你的方案也是可靠的。
反射攻击是什么
如果不使用饿汉不使用枚举做单例那我们要么做静态内部类要么做双重锁volatile来保证线程安全。同时无论是饿汉还是懒汉只要不用枚举我们都需要做私有构造函数。如下
//静态内部类实现方式
private Singleton(){} //不安全的点
public Singleton getInstance(){return Instance.INSTANCE;
}
private static class Instance{private static final Singleton INSTANCE new Singleton();
}//双重锁实现方式
private static volatile Singleton instance null;
private Singleton(){} //不安全的点
public Singleton getInstance(){if(instance null){synchronized(Singleton.class){if(instance null){instance new Singleton();}}}
}//饿汉实现方式
private static final Singleton INSTANCE new Singleton();
private Singleton(){} //不安全的点
public Singleton getInstance(){return INSTANCE;
}上面的三种实现方式注意私有构造函数这里加了注释是不安全的本意是防止被调用者直接new Singleton()创建对象设置为私有在一般情况下这是没问题的。 但是用心良苦的人可能会这么调用你
for(int i0;i10000000;i){Singleton.class.newInstance();//用反射绕过私有构造函数直接创建对象
}这个时候你的业务是不是会Denial of service 所以这很坑但是你会说我写的单例代码在java服务器内部怎么会被人这么调用这是不可能发生的没错这没问题。如果你的代码是开源的你怎么知道那些内心黑暗的人会不会从某个http接口伪造什么东西来触发newInstance()仔细想想这两年有多少人被FastJSON坑的大晚上不能安心睡觉要紧急升级代码恐怕开发阿里爸爸FastJSON团队也不想出现这样的状况。 所以我们要严谨
private Singleton(){throw new IllegalStateException();
}当然这个时候是无法使用双重锁volatile方式创建单例的因为自身调用也会抛异常。所以直接用静态内部类方式解决问题。
//静态内部类实现方式
private Singleton(){if(Instance.INSTANCE!null){throw new IllegalStateException();//这下安全了}
}
public Singleton getInstance(){return Instance.INSTANCE;
}
private static class Instance{private static final Singleton INSTANCE new Singleton();
}序列化反序列化会怎样
直接上代码
public class Singleton implements Serializable{private Singleton() {if (Instance.INSTANCE ! null) {//这里我可是防止了反射攻击哦throw new IllegalStateException();}}public static Singleton getInstance() {return Instance.INSTANCE;}private static class Instance {private static final Singleton INSTANCE new Singleton();}public static void main(String[] args) throws IOException, ClassNotFoundException {File file new File(~/Desktop/Singleton.bin);Singleton singleton Singleton.getInstance();ObjectOutputStream oos new ObjectOutputStream(new FileOutputStream(file));oos.writeObject(singleton);ObjectInputStream ois new ObjectInputStream(new FileInputStream(file));Singleton singleton1 (Singleton) ois.readObject();System.out.println(singleton singleton1);}
}这时候是不是又会Denial of service 为啥会这样其实很简单只要实现Serializable接口的类对象ObjectOutputStream会毫不犹豫的吐成字节或者读回来它才不管你是不是单例当然它也不知道内存中有这么一个单例直接在内存中创建对象了。 如何解决这个问题
public class Singleton implements Serializable{private Singleton() {if (Instance.INSTANCE ! null) {throw new IllegalStateException();}}public static Singleton getInstance() {return Instance.INSTANCE;}//实现readResolve接口就ok了private Object readResolve() throws ObjectStreamException {return Instance.INSTANCE;}private static class Instance {private static final Singleton INSTANCE new Singleton();}public static void main(String[] args) throws IOException, ClassNotFoundException {File file new File(/Users/baodi/Desktop/Singleton.bin);Singleton singleton Singleton.getInstance();ObjectOutputStream oos new ObjectOutputStream(new FileOutputStream(file));oos.writeObject(singleton);ObjectInputStream ois new ObjectInputStream(new FileInputStream(file));Singleton singleton1 (Singleton) ois.readObject();System.out.println(源对象singleton 反序列化对象 singleton1吗(singleton singleton1));}
实现readResolve返回静态内部类的对象就可以了。 看看源码我用的jdk1.81.6、1.7也一样ObjectOutputStream.readObject()中会调用内部私有方法readObject0()其中byte tc是把对象头读出来 通过switch case(tc)判断对象的类型这里我们用的是OBJECT类型魔数为0x73 这时候会调用内部私有的readOrdinaryObject()方法 这里就是调用我们重写的readResolve()方法啦 这就是jdk的大佬们为提供的一个hook方法我们可以用它保证序列化和反序列化的安全。 有人一定会说你有病序列化一个单例不我没病只是你没用到过而已 有人也会说用枚举不就屏蔽这些问题了吗不如果JDK1.6的情况下是不能把枚举当做单例对象玩的。
好了到这里就结束了吗 不记得给你的单例类加上final防止被继承后重写
结论
面试不只是刷刷题就ok了请认真的对待你写过的每一个代码因为你很可能把别人坑了比如FastJSON做技术要刨根问底网上的资料都是说如何序列化不安全的问题并没有给出ObjectOutputStream.readObject()执行原理分析不信你搜严谨、夯实基础