单例模式有饿汉式和懒汉式,这里只说懒汉式(面试总爱问到)
Java单例的三种经典实现
1、枚举
public enum EnumSingleton {
INSTANCE;
public void talk() {
System.out.println("This is an EnumSingleton " + this.hashCode());
}
}
枚举内每一个成员变量都是每个枚举自身的实例,每一个枚举都继承自java.lang.Enum类,枚举的每个成员默认都是 public static final 的。
总结:枚举实现单例是最简单也是最安全的。后面会说到
2、静态内部类
public class StaticInnerClass {
// 私有的构造器
private StaticInnerClass() {
}
//获取实例
public static StaticInnerClass instance(){
// 饿汉式进行初始化
return InnerClass.instance;
}
//静态内部类 JVM初始化加载时候,是线程安全的
private static class InnerClass{
// 当内部类加载时候,初始化成员变量
private static StaticInnerClass instance = new StaticInnerClass();
}
}
这里附上类加载顺序
3、双重检查锁(DCL)
/*
并发编程的三大特性:原子性、可见性、有序性
*/
public class DCL {
private DCL() {
}
// volatile 保证可见性和有序性
private static volatile DCL instance = null;
public static DCL getInstance(){
if (instance != null) return instance;
//synchronized 保证原子性
synchronized (DCL.class){
if (instance == null) {
/*
volatile有序性的作用
new DCL() 在jvm中解析为
1、创建实例 在堆中开辟空间
2、属性初始化
3、将对象的内存地址赋值给instance
第二步和第三步没有必然关联性,顺序可能颠倒
(jvm为了提升性能,编写的代码可能喝执行的顺序不一致、反生了指令重排序)
*/
/*
volatile可见性的作用(前提条件 多CPU情况下)
可见性是由于 内存和cup(高速缓存 )交互产生的问题
通过MESI协议解决 关于MESI协议后续更新
*/
instance = new DCL();
}
}
return instance;
}
}
破坏单例
1、反射攻击
// 通过反射,获取单例类的私有构造器
Constructor constructor = DCL.class.getDeclaredConstructor();
// 设置私有成员的暴力破解
constructor.setAccessible(true);
// 通过反射去创建单例类的多个不同的实例
DCL s1 = (DCL) constructor.newInstance();
// 通过反射去创建单例类的多个不同的实例
DCL s2 = (DCL) constructor.newInstance();
s1.tellEveryone();
s2.tellEveryone();
System.out.println(s1 == s2);
2、序列化攻击
// 对象序列化流去对对象进行操作
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("file"));
// 通过单例代码获取一个对象
DCL s1 = DCL.getSingletonInstance();
// 将单例对象,通过序列化流,序列化到文件中
outputStream.writeObject(s1);
// 通过序列化流,将文件中序列化的对象信息读取到内存中
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("file")));
// 通过序列化流,去创建对象
DCL s2 = (DCL) inputStream.readObject();
s1.tellEveryone();
s2.tellEveryone();
System.out.println(s1 == s2);
总结:以上两种攻击对枚举是无效的,枚举不能通过反射得到对象,枚举不能序列化得到对象,都会报错