JUC并发编程学习笔记(十七)彻底玩转单例模式

打印 上一主题 下一主题

主题 854|帖子 854|积分 2562

彻底玩转单例模式

单例中最重要的思想------->构造器私有!
恶汉式、懒汉式(DCL懒汉式!)
恶汉式
  1. package single;
  2. //饿汉式单例(问题:因为一上来就把对象加载了,所以可能会导致浪费内存)
  3. public class Hungry {
  4.     /*
  5.     * 如果其中有大量的需要开辟的空间,如new byte[1024*1024]这些,那么一开始就会加载,而不是需要时才加载,所以非常浪费空间
  6.     *
  7.     * */
  8.     private byte[] data1 = new byte[1024*1024];
  9.     private byte[] data2 = new byte[1024*1024];
  10.     private byte[] data3 = new byte[1024*1024];
  11.     private byte[] data4 = new byte[1024*1024];
  12.     private Hungry() {
  13.     }
  14.     private final static Hungry HUNGRY = new Hungry();
  15.     public static Hungry getInstance(){
  16.         return HUNGRY;
  17.     }
  18. }
复制代码
懒汉式
DCL懒汉式
完整的双重检测锁模式的单例、懒汉式、DCL懒汉式
  1. package single;
  2. public class LazyMan {
  3.     private LazyMan() {
  4.         System.out.println(Thread.currentThread() + "ok");
  5.     }
  6.     private volatile static LazyMan lazyMan;
  7.     //    单线程下确实ok
  8.     public static LazyMan getInstance() {
  9. //        加锁、锁整个类
  10. //        双重检测锁模式的单例、懒汉式、DCL懒汉式
  11.         if (lazyMan==null){
  12.             synchronized (LazyMan.class){
  13.                 if (lazyMan == null) {
  14.                     lazyMan = new LazyMan();//不是原子性操作
  15.                 }
  16.             }
  17.         }
  18.         return lazyMan;
  19.     }
  20.     /*
  21.      * 1、分配内存空间
  22.      * 2、执行构造方法,初始化对象
  23.      * 3、把这个对象指向这个空间
  24.      *
  25.      * 期望的结果:1、2、3
  26.      * 但是由于指令重排可能导致结果为1、3、2,这在cpu中是没问题的
  27.      * 线程A:1、3、2
  28.      * 线程B如果在线程A执行到3时开始执行判断是否为null,由于已经占用空间了,所以会被判断为不为空,但实际还未初始化对象,实际结果还是为null
  29.      *
  30.      *
  31.      * */
  32.     //    多线程并发测试
  33.     public static void main(String[] args) {
  34.         for (int i = 0; i < 10; i++) {
  35.             new Thread(() -> {
  36.                 LazyMan.getInstance();
  37.             }).start();
  38.         }
  39.     }
  40. }
复制代码
但是有反射!只要有反射,任何的代码都不安全,任何的私有关键字都是摆设
正常的单例模式:
  1. /*
  2. * 正常的单例模式创建的都为同一个对象,并且该对象全局唯一
  3. * 只执行一次创建,并且对象都是同一个
  4. * Thread[main,5,main]ok
  5. * true
  6. * */
  7. LazyMan instance1 = LazyMan.getInstance();
  8. LazyMan instance2 = LazyMan.getInstance();
  9. System.out.println(instance2==instance1);
复制代码
反射破坏单例:
  1. /*
  2. * 通过反射破坏单例
  3. * 执行两个创建,两个不同的对象
  4. * Thread[main,5,main]ok
  5.   Thread[main,5,main]ok
  6.   false
  7. * */
  8. LazyMan instance1 = LazyMan.getInstance();
  9. Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
  10. declaredConstructor.setAccessible(true);
  11. LazyMan instance2 = declaredConstructor.newInstance();
  12. System.out.println(instance2 == instance1);
复制代码
怎么去解决这种破坏呢?
首先反射走了无参构造器,我们可以在构造器中进行加锁判断是否已经存在了对象。
  1. private LazyMan() {
  2.     //通过构造器来加锁判断防止反射破坏
  3.     synchronized (LazyMan.class){
  4.         if (lazyMan!=null){
  5.             throw new RuntimeException("不要试图使用反射破坏单例模式");
  6.         }
  7.     }
  8. }
复制代码
通过反射破坏单例模式
道高一尺,魔高一丈
1、通过普通的反射来破坏单例模式
  1. Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
  2. declaredConstructor.setAccessible(true);
  3. LazyMan lazyMan1 = LazyMan.getInstance();
  4. LazyMan lazyMan2 = declaredConstructor.newInstance();
  5. System.out.println(lazyMan1);
  6. System.out.println(lazyMan2);
复制代码

解决方法:通过构造器加锁解决
  1. private LazyMan() {
  2.     //通过构造器来加锁判断防止反射破坏
  3.     synchronized (LazyMan.class){
  4.         if (lazyMan == null){
  5.         }else {
  6.             throw new RuntimeException("不要试图使用反射破坏单例模式");
  7.         }
  8.     }
  9. }
复制代码
2、通过反射创建两个类来破坏单例模式
  1. Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
  2. declaredConstructor.setAccessible(true);
  3. LazyMan lazyMan1 = declaredConstructor.newInstance();
  4. LazyMan lazyMan2 = declaredConstructor.newInstance();
  5. System.out.println(lazyMan1);
  6. System.out.println(lazyMan2);
复制代码
解决方法:设置一个外部私有变量,在构造方法中通过外部私有变量来操作
  1. //创建一个外部的标,用于防止通过newInstance破坏单例模式
  2. private static boolean flg = true;
  3. private LazyMan() {
  4.     //通过构造器来加锁判断防止反射破坏
  5.     synchronized (LazyMan.class){
  6.         if (flg){
  7.             flg = false;
  8.         }else {
  9.             throw new RuntimeException("不要试图使用反射破坏单例模式");
  10.         }
  11.     }
  12. }
复制代码
3、通过反射字段来将外部私有变量修改。
  1. Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
  2. declaredConstructor.setAccessible(true);
  3. //通过反射修改内部私有变量
  4. Field flg1 = LazyMan.class.getDeclaredField("flg");
  5. flg1.setAccessible(true);
  6. //通过反射的newInstance创建的两个对象依旧破坏了单例模式
  7. LazyMan instance1 = declaredConstructor.newInstance();
  8. //通过反射字段对单例模式进行破坏
  9. flg1.set(instance1,true);
  10. LazyMan instance2 = declaredConstructor.newInstance();
  11. System.out.println(instance2 == instance1);
复制代码
解决方法,通过枚举类型!枚举类型自带单例模式,禁止反射破坏
  1. package single;
  2. import java.lang.reflect.Constructor;
  3. import java.lang.reflect.InvocationTargetException;
  4. //枚举类
  5. public enum EnumDemo {
  6.     INSTANCE;
  7.     public EnumDemo getInstance(){
  8.         return INSTANCE;
  9.     }
  10. }
  11. class EnumDemoTest{
  12.     public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
  13.         Constructor<EnumDemo> declaredConstructor = EnumDemo.class.getDeclaredConstructor(null);
  14.         declaredConstructor.setAccessible(true);
  15.         EnumDemo enumDemo1 = declaredConstructor.newInstance();
  16.         EnumDemo enumDemo2 = declaredConstructor.newInstance();
  17.         System.out.println(enumDemo1);
  18.         System.out.println(enumDemo2);
  19.     }
  20. }
复制代码
发现抱错,没有对应的无参构造

但是idea编译的源码中是由无参构造的

idea欺骗了我们,那么编译好的类到底有没有无参构造,通过javap -p反编译源码查看所以方法

可以看到,也有空参的构造方法,也就意味了反编译源码也欺骗了你,所以我们通过更专业的工具来查看,使用jad查看。

查看当前目录新生成的java文件可以发现,通过jad反编译的源码的构造函数时个有参构造函数
  1. // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
  2. // Jad home page: http://www.kpdus.com/jad.html
  3. // Decompiler options: packimports(3)
  4. // Source File Name:   EnumDemo.java
  5. package single;
  6. public final class EnumDemo extends Enum
  7. {
  8. public static EnumDemo[] values()
  9. {
  10.     return (EnumDemo[])$VALUES.clone();
  11. }
  12. public static EnumDemo valueOf(String name)
  13. {
  14.     return (EnumDemo)Enum.valueOf(single/EnumDemo, name);
  15. }
  16. private EnumDemo(String s, int i)
  17. {
  18.     super(s, i);
  19. }
  20. public EnumDemo getInstance()
  21. {
  22.     return INSTANCE;
  23. }
  24. public static final EnumDemo INSTANCE;
  25. private static final EnumDemo $VALUES[];
  26. static
  27. {
  28.     INSTANCE = new EnumDemo("INSTANCE", 0);
  29.     $VALUES = (new EnumDemo[] {
  30.         INSTANCE
  31.     });
  32. }
  33. }
复制代码
我们尝试在反射中加入这两个参数类
  1. Constructor<EnumDemo> declaredConstructor = EnumDemo.class.getDeclaredConstructor(String.class,int.class);
复制代码
可以发现,它根据我们预想的结果抛出一个异常

在newInstance方法中如果时枚举类就会抛出这个异常,这是从反射层面限制了对枚举类单例模式的破坏!!


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

雁过留声

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表