单例模式的挑战:反射和序列化
参考1: 你写的单例模式,能防止反序列化和反射吗?
参考2:枚举实现单例
常见单例模式
// 饿汉,在类加载的时候就被实例化
/**
* 恶汉式单例,线程安全
* @author sicimike
* @create 2020-02-23 20:15
*/
public class Singleton1 {
private static final Singleton1 INSTANCE = new Singleton1();
private Singleton1() {}
public static Singleton1 getInstance() {
return INSTANCE;
}
}
/**
* 饿汉式单例,静态代码块,线程安全
* @author sicimike
* @create 2020-02-23 20:19
*/
public class Singleton2 {
private static Singleton2 INSTANCE = null;
static {
INSTANCE = new Singleton2();
}
private Singleton2() {}
public static Singleton2 getInstance() {
return INSTANCE;
}
}
// 懒汉式单例
/**
* 懒汉式单例,线程安全
* 双重校验锁
* @author sicimike
* @create 2020-02-23 20:34
*/
public class Singleton6 {
private static volatile Singleton6 INSTANCE = null;
private Singleton6() {}
public static Singleton6 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton6.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
}
不加volatile关键字 线程不安全,根本原因就是INSTANCE = new Singleton5()不是原子操作。而是分为三步完成
1、分配内存给这个对象
2、初始化这个对象
3、把INSTANCE变量指向初始化的对象
正常情况下按照1 -> 2 -> 3的顺序执行,但是2和3可能会发生重排序,执行顺序变成1 -> 3 -> 2。如果是1 -> 3 -> 2的顺序执行。线程A执行完3,此时对象尚未初始化,但是INSTANCE变量已经不为null,线程B执行到synchronized关键字外部的if判断时,就直接返回了。此时线程B拿到的是一个尚未初始化完成的对象,可能会造成安全隐患。所以这种实现方式是线程不安全的。
volatile关键字的在这里的作用有两个:
解决了重排序的问题
保证了INSTANCE的修改,能够及时的被其他线程所知
静态内部类方式
既满足懒加载,又满足线程安全,代码量还少,相对来说是一种比较优雅的实现方式
/**
* 懒汉式单例,线程安全
* 静态内部类
* @author sicimike
* @create 2020-02-23 20:36
*/
public class Singleton7 {
private Singleton7() {}
public static Singleton7 getInstance() {
return InnerClass.INSTANCE;
}
private static class InnerClass {
private static Singleton7 INSTANCE = new Singleton7();
}
}
枚举方式
public enum DataSourceEnum {
DATASOURCE;
private DBConnection connection = null;
private DataSourceEnum(){
connection = new DBConnection();
}
public DBConnection getConnection(){
return connection;
}
}
public class DBConnection {
}
// 测试 返回 true
public class Test {
public static void main(String[] args) {
DBConnection conn1 = DataSourceEnum.DATASOURCE.getConnection();
DBConnection conn2 = DataSourceEnum.DATASOURCE.getConnection();
System.out.println(conn1 == conn2);
}
}
反射
反射会破坏单例
public void reflectSingleton1(){
try {
Object compare1 = Singleton1.getInstance();
Class<?> tClass = Singleton1.class;
Constructor constructor = tClass.getDeclaredConstructor(null);
constructor.setAccessible(true);
Object instance = constructor.newInstance();
System.out.println(instance);
System.out.println(compare1);
System.out.println(instance == compare1);
} catch (Exception e){
e.printStackTrace();
}
}
// 输出
singleton.Singleton1@27f674d
singleton.Singleton1@1d251891
false
// 添加如下报错内容处理,防止通过反射初始化单例对象,但是不够优雅
private Singleton1() {
if(INSTANCE != null){
throw new RuntimeException("do not xia gao");
}
}
序列化
序列化也会破坏单例,不再举例。
可在Singleton1内添加如下方法
private Object readResolve(){
return this.INSTANCE;
}
经过源码查看,若目标类有readResolve方法,那就通过反射的方式调用要被反序列化的类中的readResolve方法,返回一个对象,然后把这个新的对象复制给最终返回的对象。
因此,新建readResolve方法,返回单例类,即保证还是原来创建的类,没有创建新类,是一个对象。
最优解:就是用枚举
// 可以看一下DataSourceEnum类的反编译代码
public final class DataSourceEnum extends Enum
{
public static DataSourceEnum[] values(){
return (DataSourceEnum[])$VALUES.clone();
}
//toString的逆方法,返回指定名字,给定类的枚举常量
public static DataSourceEnum valueOf(String name){
return (DataSourceEnum)Enum.valueOf(creational/singleton/dbconn/DataSourceEnum, name);
}
//私有构造函数,参数有 此枚举常量的名称,枚举常量的序号
private DataSourceEnum(String s, int i){
super(s, i);
//单例对象的属性
connection = null;
connection = new DBConnection();
}
public DBConnection getConnection(){
return connection;
}
//单例对象
public static final DataSourceEnum DATASOURCE;
//单例对象的属性
private DBConnection connection;
private static final DataSourceEnum $VALUES[];
static
{
//与饿汉式相似,类初始化时创建单例对象
DATASOURCE = new DataSourceEnum("DATASOURCE", 0);
$VALUES = (new DataSourceEnum[] {
DATASOURCE
});
}
}
Java规范字规定,每个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。在序列化的时候Java仅仅是将枚举对象的name属性输到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。也就是说,序列化的时候只将DATASOURCE这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。