单列模式注意事项与破解

本篇博文需要有java并发,类加载过程,java反射的一些知识

单列确保系统中一个类只用一个实列

单列模式提供的全局访问方法可以认为使用了简单工厂模式,简单点来说单列模式自己负责创建自己

单列模式的创建方式

由于本文主要讲解如何破解单列模式,所以在这里就不详细介绍,简单的列举5种创建方式与特点

  1. 饿汉式 – 线程安全不延时加载

  2. 懒汉式 –线程不安全延时加载 –可能存在引用还没完全初始化的对象

  3. 双重检索机制 –线程相对安全延时加载

  4. 静态内部类 –线程安全延时加载 –由于对象变量为静态变量在类加载过程中的初始化阶段虚拟机保证一个类的()方法在多线程中被正确的加锁,同步–>线程安全。–内部类在虚拟机中特性为延迟加载

  5. 枚举模式 –线程安全不能延时加载天然单列模式不可通过反射序列化反序列化破解

双重检索机制

核心代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Single {
private static Single instance=null;
//设置构成函数私有 不对外暴露 使getInstance成为全局访问入口
private Single(){};
public static Single getInstance(){
//以下为双从检索核心代码
//先检测实例引用是否为空
if(instance==null){
//双重检索主要为了降低系统开销
synchronized (Single.class){ //用类做锁 确保线程异步
if (instance==null){ //进行二次判断
instance=new Single();
}
}
}
return instance;
}
public static void main(String[] args) {
//开启了30个线程池 用来检测检测线程安全
ExecutorService exe= Executors.newFixedThreadPool(30);
for (int i = 0; i < 100; i++) {
exe.execute(()->{
System.out.println(Single.getInstance().hashCode()+"->"+Thread.currentThread().getName());
});
}
exe.shutdown();
}
}

为什么说双重检索机制线程不安全呢,因为在多线程中存在指令重排序 –> 我们可以使用volatile关键字禁止重排序

指令重排序 –>编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段 指令重排序的前提是在单线程中不影响后续计算要求 比如 a=10;a=12便存在指令重排序的可能,a=10;a=12;b=a;便不会重排序

这里与指令重拍有什么关联呢 –>

当虚拟机运行到new操作时,会到常量池中寻找该类的符号,若没有则需要先加载,加载后存入方法区–存放已加载的类信息

  1. 在堆内存中分配内存空间

  2. 初始化对象(不是类加载过程中的初始化) 执行构造方法

  3. 设置实例指向刚分配的内存 –与上一步可重排序

    若第二步与第三步实现重排序则有可能会出现以下表格状态则会使线程B引用还没完全初始化的对象

    –>多线程不安全

    | 线程A | 线程B |
    | ——————– | ————————— |
    | 分配对象的内存空间 | |
    | 设置实例指向内存空间 | |
    | | 判断实例是否为空 此时不为空 |
    | | 引用实列 |
    | 初始化对象 | |

    反射破解单列模式

    使用静态内部类的方式实现代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Single {
private Single(){}
private static class Instance{
private static Single instance=new Single();
}
public static Single getInstance(){
return Instance.instance;
}
public static void main(String[] args) {
Class clazz=Single.class; //获得Class对象 每一个类都是Class类的对象
try {
//获得参数为空构造方法可获得私有的
Constructor constructor=clazz.getDeclaredConstructor(new Class[]{});
//取消权限访问检查
constructor.setAccessible(true);
//获得对象实例
Single s1= (Single) constructor.newInstance(new Object[]{});
Single s2= (Single) constructor.newInstance(new Object[]{});
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
//正常方法获得对象实列
Single s3=Single.getInstance();
Single s4=Single.getInstance();
System.out.println(s3.hashCode());
System.out.println(s4.hashCode());
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}

运行结果如下所示

1
2
3
4
1746572565
989110044
424058530
424058530
我们可以看出使用反射获得的不在是同一实列 –>构造函数中抛出RunException可防止

###

序列化与反序列化破解单列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Single implements Serializable{//特别注意这里使用了 序列化接口标识
private Single(){

}
private static class Instance {
private static Single instance=new Single();
}
public static Single getInstance(){
return Instance.instance;
}
public static void main(String[] args) {
//正常方法获得对象实列
Single s3=Single.getInstance();
Single s4=Single.getInstance();
System.out.println(s3.hashCode());
System.out.println(s4.hashCode());
try {
//序列化 把对象转换成字节序列的过程 将对象状态保存
OutputStream out=new FileOutputStream("f:/桌面/test.txt");
ObjectOutputStream outObj=new ObjectOutputStream(out);
outObj.writeObject(s4);
outObj.close();
// 将字节序列转换成序列 恢复对象状态
InputStream in=new FileInputStream("f:/桌面/test.txt");
ObjectInputStream inObj=new ObjectInputStream(in);
Single s5= (Single) inObj.readObject();
System.out.println(s5.hashCode());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

运行结果输出如下

1
2
3
4
5
6
7
8
1746572565
1746572565
1560911714

加入以下方法可以防止
private Object readResolve() {
return INSTANCE;
}

使用枚举模式天然保证单列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public enum Single implements Serializable{
INSTANCE;
public static void main(String[] args) {
//正常方法获得对象实列
Single s3= Single.INSTANCE;
Single s4=Single.INSTANCE;
System.out.println(s3.hashCode());
System.out.println(s4.hashCode());
try {
//序列化 把对象转换成字节序列的过程 将对象状态保存
OutputStream out=new FileOutputStream("f:/桌面/test.txt");
ObjectOutputStream outObj=new ObjectOutputStream(out);
outObj.writeObject(s4);
outObj.close();
// 将字节序列转换成序列 恢复对象状态
InputStream in=new FileInputStream("f:/桌面/test.txt");
ObjectInputStream inObj=new ObjectInputStream(in);
Single s5= (Single) inObj.readObject();
System.out.println(s5.hashCode());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
文章目录
  1. 1. 单列模式的创建方式
  2. 2. 双重检索机制
    1. 2.0.1. 在堆内存中分配内存空间
    2. 2.0.2. 初始化对象(不是类加载过程中的初始化) 执行构造方法
    3. 2.0.3. 设置实例指向刚分配的内存 –与上一步可重排序
  • 3. 反射破解单列模式
    1. 3.0.0.1. 我们可以看出使用反射获得的不在是同一实列 –>构造函数中抛出RunException可防止
  • 4. 序列化与反序列化破解单列
  • 5. 使用枚举模式天然保证单列
  • ,