暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

深入单例模式及安全攻防

一叶扁舟 2022-10-09
444

image.png

一、单例模式概述

1.1、概述

  • 概述

    单例模式,是设计模式中最常见的模式之一,它是一种创建对象模式,用于产生一个对象的具体实例,可以确保系统中一个类只会产生一个实例

  • 优缺点

    【优点】

    1、对于频繁使用的对象,可以省去 new 操作花费的时间,尤其对那些重量级对象而言,削减了一笔非常客观的系统开销。

    2、由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,从而减轻 GC 压力缩短 GC 停顿时间

    【缺点】

    1、单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。

    2、在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。

    3、单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

  • 应用场景

    1. Spring中创建的 Bean 实例默认都是单例。
    2. 数据库连接池的设计与实现。
    3. 多线程的线程池设计与实现。
  • 核心实现点

    构造方法私有化

1.2、分类

单例模式常见分为两大类:饿汉式、懒汉式

1.2.1、饿汉式

JVM 对**类加载的时候**,单例对象就会被创建,需要用的时候直接拿过来用就好了

  • 优缺点

    【优点】

    获取对象的速度快线程安全

    【缺点】

    耗内存(初始化时就讲对象实例化了,占用了内存)

  • 为什么饿汉式是线程安全的?

    JVM 对类加载的时候,单例对象就会被创建,类加载的过程是线程安全的。

1.2.2、懒汉式

先不急着创建对象,在需要**使用时候再去创建**。

  • 优缺点

    【优点】

    只有在使用时才被实例化,节省了资源

    【缺点】

    线程不安全

二、常见写法

2.1、饿汉式

线程安全

反射无法破坏单例

public class HungryDemo { /** * 如果对象很大,一加载对象时就实例化,会浪费空间 */ private final byte[] data1 = new byte[1024 * 1024]; private final byte[] data2 = new byte[1024 * 1024]; /** * 私有化构造器 */ private HungryDemo() { System.out.println("create Singleton"); } /** * 类加载时就创建对象 */ private static final HungryDemo HUNGRY = new HungryDemo(); public static HungryDemo getInstance(){ return HUNGRY; } }
复制

2.2、懒汉式

线程不安全

反射能破坏单例

2.2.1、实现

public class LazyDemo { /** * 私有化构造器 */ private LazyDemo() { System.out.println("create Singleton"); } /** * 不直接创建对象 */ private static LazyDemo lazyDemo; public static LazyDemo getInstance() { if (lazyDemo == null) { lazyDemo = new LazyDemo(); } return lazyDemo; } }
复制

2.2.2、线程不安全测试

public class LazyDemo { /** * 私有化构造器 */ private LazyDemo() { System.out.println(Thread.currentThread().getName() + "create Singleton"); } /** * 不直接创建对象 */ private static LazyDemo lazyDemo; public static LazyDemo getInstance() { if (lazyDemo == null) { lazyDemo = new LazyDemo(); } return lazyDemo; } /** * 多线程并发测试 * @param args args */ public static void main(String[] args) { for (int i = 0; i < 20; i++) { new Thread(() -> { LazyDemo.getInstance(); }).start(); } } }
复制

上面测试结果如下:

Thread-0create Singleton Thread-1create Singleton
复制

可看出,创建了2个对象,即单例模式被多线程破坏了

2.3、双重检查加锁(DCL)

线程安全

反射能破坏单例

3.3.1、实现

public class DCLDemo { /** * 私有化构造器 */ private DCLDemo() { System.out.println(Thread.currentThread().getName() + "create Singleton"); } /** * 不直接创建对象 * 此处必须加volatile */ private volatile static DCLDemo dclDemo; public static DCLDemo getInstance() { // 1重校验 if (dclDemo == null) { // 对DCLDemo加锁 synchronized (DCLDemo.class) { // 2重校验 if (dclDemo == null) { // 创建对象 dclDemo = new DCLDemo(); } } } return dclDemo; } /** * 多线程并发测试 * @param args args */ public static void main(String[] args) { for (int i = 0; i < 20; i++) { new Thread(() -> { DCLDemo.getInstance(); }).start(); } } }
复制

📢注意点:必须加volatile

在创建对象那一步,实际上是三步操作:

(1)分配内存空间

(2)执行构造方法,初始化对象

(3)把对象指向这个空间

在此过程中,可能会发生指令重排,从而变成1、3、2这样的一个步骤,

在多线程下,比如线程A执行了1、3步骤,此时线程B进来了,经过判定,dclDemo != null,那么将会跳过创建对象,直接return,而此时第2步还没执行(dclDemo还没完成构造),那么就会返回一个空的内存空间。

3.3.2、反射破坏

使用反射,可以获得两个对象

public class DCLDemo { /** * 私有化构造器 */ private DCLDemo() { System.out.println(Thread.currentThread().getName() + "create Singleton"); } /** * 不直接创建对象 * 此处必须加volatile */ private volatile static DCLDemo dclDemo; public static DCLDemo getInstance() { // 1重校验 if (dclDemo == null) { // 对DCLDemo加锁 synchronized (DCLDemo.class) { // 2重校验 if (dclDemo == null) { // 创建对象 dclDemo = new DCLDemo(); } } } return dclDemo; } /** * 反射破解DCL * @param args args */ public static void main(String[] args) throws Exception { DCLDemo dclDemo1 = DCLDemo.getInstance(); Constructor<DCLDemo> declaredConstructor = DCLDemo.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); DCLDemo dclDemo2 = declaredConstructor.newInstance(); System.out.println(dclDemo1 == dclDemo2); // fasle } }
复制

3.3.3、进阶实现

在构造器里加锁和判断,当对象已经存在时,就不能通过构造器构造新的对象了

public class DCLDemo { /** * 私有化构造器 */ private DCLDemo() { synchronized (DCLDemo.class) { if (dclDemo != null) { throw new RuntimeException("不要试图使用反射破坏异常"); } } System.out.println(Thread.currentThread().getName() + "create Singleton"); } ...... }
复制

报错如下:

Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.example.demo1.test.singleton.DCLDemo.main(DCLDemo.java:52) Caused by: java.lang.RuntimeException: 不要试图使用反射破坏异常 at com.example.demo1.test.singleton.DCLDemo.<init>(DCLDemo.java:18) ... 5 more
复制

3.3.4、反射进阶破坏

不创建对象,两个对象都是通过getDeclaredConstructor获取

public class DCLDemo { ...... /** * 反射破解DCL * @param args args */ public static void main(String[] args) throws Exception { Constructor<DCLDemo> declaredConstructor = DCLDemo.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); DCLDemo dclDemo1 = declaredConstructor.newInstance(); DCLDemo dclDemo2 = declaredConstructor.newInstance(); System.out.println(dclDemo1 == dclDemo2); // fasle } }
复制

3.3.5、再次进阶实现

使用一个标志变量,不通过反编译的方式,是无法破解这个标志变量名和值的。

public class DCLDemo { private static boolean flag = false; /** * 私有化构造器 */ private DCLDemo() { synchronized (DCLDemo.class) { if (!flag) { flag = true; } else { throw new RuntimeException("不要试图使用反射破坏异常"); } } System.out.println(Thread.currentThread().getName() + "create Singleton"); } ....... }
复制

报错如下:

Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.example.demo1.test.singleton.DCLDemo.main(DCLDemo.java:58) Caused by: java.lang.RuntimeException: 不要试图使用反射破坏异常 at com.example.demo1.test.singleton.DCLDemo.<init>(DCLDemo.java:23) ... 5 more
复制

3.3.6、反射再次进阶破坏

如果被反编译或者泄露了,那还是可以通过反射破坏

public class DCLDemo { ...... /** * 反射破解DCL * @param args args */ public static void main(String[] args) throws Exception { // 获取字段并破坏私有权限 Field flag = DCLDemo.class.getDeclaredField("flag"); flag.setAccessible(true); Constructor<DCLDemo> declaredConstructor = DCLDemo.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); DCLDemo dclDemo1 = declaredConstructor.newInstance(); // 获得第一个对象之后,再把flag值改回来 flag.set(dclDemo1, false); DCLDemo dclDemo2 = declaredConstructor.newInstance(); System.out.println(dclDemo1 == dclDemo2); // fasle } }
复制

2.4、静态内部类

线程安全

反射能破坏单例

public class OuterClassDemo { /** * 构造器私有 */ private OuterClassDemo() { System.out.println(Thread.currentThread().getName() + "create Singleton"); } /** * 匿名内部类 */ public static class InnerClass { private static final OuterClassDemo outerClassDemo = new OuterClassDemo(); } public static OuterClassDemo getInstance() { return InnerClass.outerClassDemo; } /** * 多线程并发测试 * @param args args */ public static void main(String[] args) { for (int i = 0; i < 20; i++) { new Thread(() -> { OuterClassDemo.getInstance(); }).start(); } } }
复制

2.5、枚举

线程安全

反射不能破坏单例

/** * 枚举类 */ public enum EnumSingleton { INSTANCE; public static EnumSingleton getInstance(){ return INSTANCE; } } /** * 测试反射破坏 */ class Test{ public static void main(String[] args) throws Exception { EnumSingleton instance1 = EnumSingleton.INSTANCE; Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class); // 破坏私有权限 declaredConstructor.setAccessible(true); EnumSingleton instance2 = declaredConstructor.newInstance(); System.out.println(instance1 == instance2); } }
复制

反射破坏会报错:Cannot reflectively create enum objects

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.lang.reflect.Constructor.newInstance(Constructor.java:417) at com.example.demo1.test.singleton.Test.main(EnumSingleton.java:32)
复制

三、总结

  • 单例模式需要处理的问题

    1. 将构造函数私有化
    2. 通过静态方法获取一个唯一实例
    3. 保证线程安全
    4. 防止反序列化造成的新实例等。
  • 常见实现方式对比

    在实际工作中,单例的使用还是比较常见的,在几种实现方式中,双重检测机制、静态内部类、枚举方式都是比较推荐。

    单例模式实现方式 是否线程安全 是否懒加载 是否能防止反射破坏
    饿汉式
    懒汉式
    双重锁检测
    静态内部类
    枚举
  • 涉及到的知识点

    单例模式

    static修饰符、synchronized修饰符(加锁)、volatile修饰符(防止指令重排)、enum等

    这里的每一个知识点都可以变成面试官下手的考点,而单例只是作为一个引子

最后修改时间:2022-10-09 10:10:52
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论

目录
  • 一、单例模式概述
    • 1.1、概述
    • 1.2、分类
      • 1.2.1、饿汉式
      • 1.2.2、懒汉式
  • 二、常见写法
    • 2.1、饿汉式
    • 2.2、懒汉式
      • 2.2.1、实现
      • 2.2.2、线程不安全测试
    • 2.3、双重检查加锁(DCL)
      • 3.3.1、实现
      • 3.3.2、反射破坏
      • 3.3.3、进阶实现
      • 3.3.4、反射进阶破坏
      • 3.3.5、再次进阶实现
      • 3.3.6、反射再次进阶破坏
    • 2.4、静态内部类
    • 2.5、枚举
  • 三、总结