彻底玩转单例模式

彻底玩转单例模式
  • 饿汉式:类加载时初始化,不存在并发访问问题,会有资源浪费
  • 懒汉式:延时加载,使用时才实例化对象,存在并发访问问题,资源利用率高
  • 双重检测锁式 :利用sychronized关键字解决了懒汉式并发访问问题,同时为了解决指令重排问题使用了volatile关键字
  • 静态内部类式:兼并并发高效调用和延迟加载的优势
  • 枚举单例:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的package 单例模式; //饿汉式 public class Hungry { private static Hungry hungry = new Hungry(); //构造器私有 private Hungry() { } public static Hungry getInstance() { return hungry; } }

    优点:static变量会在类装载时初始化,不涉及多个线程访问该对象的问题,可以省略package 单例模式; //懒汉式 public class Lazy { private static Lazy lazy; private Lazy() { } public static Lazy getInstance() { if (lazy == null) lazy = new Lazy(); return lazy; } }

    优点:延迟加载,真正用的时候才实例化对象,提高了资源的利用率

    缺点:存在并发访问的问题,以下测试并发访问情况

    package 单例模式;  //懒汉式 public class Lazy {     private static Lazy lazy;      private Lazy() {         System.out.println("创建示例");     }      public static Lazy getInstance() {         if (lazy == null)             lazy = new Lazy();         return lazy;     }      public static void main(String[] args) {         //10条线程并发访问下         for (int i = 0; i < 10; i++) {             new Thread(() -> {                 Lazy.getInstance();             }).start();         }     } } 

    彻底玩转单例模式
    根据结果,可以看到有5个线程打印了结果,也就说进行了5次初始化,这是非常大的漏洞,出现了并发访问的问题


    3. 双重检测锁式

    为了解决懒汉式并发访问的问题,加入了sychronized关键字

    package 单例模式;  //双重检测锁式 public class DoubleLock {     private static DoubleLock doubleLock;      private DoubleLock() {         System.out.println("创建示例");     }      public static DoubleLock getInstance() {         if (doubleLock == null) {             synchronized (Lazy.class) {                 if (doubleLock == null)                     doubleLock = new DoubleLock();             }         }         return doubleLock;     }      public static void main(String[] args) {         //10条线程并发访问下         for (int i = 0; i < 10; i++) {             new Thread(() -> {                 DoubleLock.getInstance();             }).start();         }     } } 

    彻底玩转单例模式
    根据打印结果,解决了并发访问的问题;但是这样仍然会存在问题,因为我们new对象时并不是一个完整的原子性操作,而是分为以下三部:

    1. 分配内存空间
    2. 执行构造方法,初始化对象
    3. 把这个对象指向这个空间

    单个线程A执行的情况下可以123按顺序执行,也可能由于指令重排按132执行;但是如果线程A按132顺序执行到3时来了一个线程B,此时该对象已经指向了分配的空间,因此B判断对象不是null,就会直接返回对象,但其实对象并没有进行初始化,就造成了错误

    因此指令重排也会导致错误,因此完整的双重检测锁式还加入了Volatile关键字来避免指令重排,完整代码如下:

    package 单例模式;  //双重检测锁式 public class DoubleLock {     private volatile static DoubleLock doubleLock;      private DoubleLock() {         System.out.println("创建示例");     }      public static DoubleLock getInstance() {         if (doubleLock == null) {             synchronized (Lazy.class) {                 if (doubleLock == null)                     doubleLock = new DoubleLock();             }         }         return doubleLock;     } } 

    4. 静态内部类式

    package 单例模式;  public class InnerClass {     private InnerClass() {      }      //静态内部类里面创建对象     public static class inner {         private static final InnerClass innerClass = new InnerClass();     }      public static InnerClass getInstance() {         return inner.innerClass;     } } 
    1. 延时加载,只有真正调用getinstance(),才会加载静态内部类。
    2. 线程安全的,Instance是static final类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性。
    3. 兼备了并发高效调用和延迟加载的优势

    — 反射破坏单例模式,引入枚举单例

    以下通过反射对双重检测锁式单例进行破坏

    package 单例模式;  import java.lang.reflect.Constructor;  //双重检测锁式 public class DoubleLock {     private volatile static DoubleLock doubleLock;      private DoubleLock() {         System.out.println("创建示例");     }      public static DoubleLock getInstance() {         if (doubleLock == null) {             synchronized (Lazy.class) {                 if (doubleLock == null)                     doubleLock = new DoubleLock();             }         }         return doubleLock;     }      public static void main(String[] args) throws Exception {         DoubleLock instance1 = doubleLock.getInstance();         Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);         constructor.setAccessible(true);         DoubleLock instance2 = constructor.newInstance();         System.out.println(instance1);         System.out.println(instance2);     } } 

    彻底玩转单例模式
    根据结果,看到创建了两个实例,也就是单例模式被破坏,那么怎么解决呢?

    可以在私有构造中加锁

    package 单例模式;  import java.lang.reflect.Constructor;  //双重检测锁式 public class DoubleLock {     private volatile static DoubleLock doubleLock;      private DoubleLock() {         synchronized (DoubleLock.class){             if(doubleLock!=null){                 throw new RuntimeException("不要试图使用反射破坏异常");             }         }         System.out.println("创建示例");     }      public static DoubleLock getInstance() {         if (doubleLock == null) {             synchronized (Lazy.class) {                 if (doubleLock == null)                     doubleLock = new DoubleLock();             }         }         return doubleLock;     }      public static void main(String[] args) throws Exception {         DoubleLock instance1 = doubleLock.getInstance();         Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);         constructor.setAccessible(true);         DoubleLock instance2 = constructor.newInstance();         System.out.println(instance1);         System.out.println(instance2);     } } 

    彻底玩转单例模式
    根据结果,可以看到避免了单例模式的破坏?可是上述两个对象一个是通过单例获取,一个通过反射获取;

    那如果两个对象都是通过反射获取呢?

    public static void main(String[] args) throws Exception {     Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);     constructor.setAccessible(true);     DoubleLock instance1= constructor.newInstance();     DoubleLock instance2 = constructor.newInstance();     System.out.println(instance1);     System.out.println(instance2); } 

    彻底玩转单例模式
    根据结果,可以看到单例模式又被破坏了,创建了两个对象!这种情况如何解决呢?

    可以通过红绿灯方法实现,定义一个标志位记录对象是否创建

    package 单例模式;  import java.lang.reflect.Constructor;  //双重检测锁式 public class DoubleLock {     private volatile static DoubleLock doubleLock;      //标志位     private static boolean flag = false;      private DoubleLock() {         synchronized (DoubleLock.class) {             if (flag == false)                 flag = true;             else                 throw new RuntimeException("不要试图使用反射破坏异常");         }         System.out.println("创建示例");     }      public static DoubleLock getInstance() {         if (doubleLock == null) {             synchronized (Lazy.class) {                 if (doubleLock == null)                     doubleLock = new DoubleLock();             }         }         return doubleLock;     }      public static void main(String[] args) throws Exception {         Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);         constructor.setAccessible(true);         DoubleLock instance1 = constructor.newInstance();         DoubleLock instance2 = constructor.newInstance();         System.out.println(instance1);         System.out.println(instance2);     } } 

    彻底玩转单例模式
    可以看到我们通过设置标志位flag再次解决了这个问题,但是一旦被获取了这个关键字,单例模式仍然可以通过反射被破解,如下所示

    public static void main(String[] args) throws Exception {     Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);     Field declaredField = DoubleLock.class.getDeclaredField("flag");     constructor.setAccessible(true);     declaredField.setAccessible(true);     DoubleLock instance1 = constructor.newInstance();     declaredField.set(instance1, false);//第一个对象创建完毕后将flag改为false     DoubleLock instance2 = constructor.newInstance();     System.out.println(instance1);     System.out.println(instance2); } 

    彻底玩转单例模式
    可以看到单例模式再次被破坏;因此为了让程序更加安全,通常对flag关键字进行加密处理

    那么到底如何完全的避免反射破坏单例模式呢?我们查看newInstance的源码
    彻底玩转单例模式
    可以看到,如果是枚举类型的话,就不能通过反射获取枚举;

    因此引入了第5种单例模式


    5. 枚举单例

    • 优点:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!

    • 缺点:无延迟加载

    package 单例模式;  import java.lang.reflect.Constructor;  //enum本质上就是一个Class类 public enum EnumSingle {     INSTANCE;      public static void main(String[] args) throws Exception {         EnumSingle instance1 = EnumSingle.INSTANCE;         Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);         declaredConstructor.setAccessible(true);         EnumSingle instance2 = declaredConstructor.newInstance();         System.out.println(instance1);         System.out.println(instance2);     } } 

    我们再次通过反射创建对象,根据结果报错没有EnumSingle的空构造方法,这不是我们希望看到的

    彻底玩转单例模式
    我们对EnumSingle的class文件进行反编译,可以看到明明有空构造方法
    彻底玩转单例模式
    但是执行明明报错没有无参构造,我们使用更专业的反编译工具jad对class文件再进行反编译
    彻底玩转单例模式
    可以看到枚举类本质上就是继承了Enum类,本身就是一个Class,而且没有无参构造,而是含两个参数的有参构造,我们修改代码在测试

    public static void main(String[] args) throws Exception {     EnumSingle instance1 = EnumSingle.INSTANCE;     Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);     declaredConstructor.setAccessible(true);     EnumSingle instance2 = declaredConstructor.newInstance();     System.out.println(instance1);     System.out.println(instance2); } 

    彻底玩转单例模式
    这才正确显示了报错的信息:无法反射地创建枚举对象

版权声明:玥玥 发表于 2021-05-12 18:47:55。
转载请注明:彻底玩转单例模式 | 女黑客导航