1.JUC并发编程学习笔记(二)Lock锁(重点)2.JUC并发编程学习笔记(一)认知进程和线程3.JUC并发编程学习笔记(三)生产者和消费者问题4.JUC并发编程学习笔记(四)8锁现象5.JUC并发编程学习(五)集合类不安全6.JUC并发编程学习笔记(六)Callable(简单)7.JUC并发编程学习笔记(七)常用的辅助类8.JUC并发编程学习笔记(八)读写锁9.JUC并发编程学习笔记(九)阻塞队列10.JUC并发编程学习笔记(十)线程池(重点)11.JUC并发编程学习(十一)四大函数式接口(必备)12.JUC并发编程学习笔记(十二)Stream流式计算13.JUC并发编程学习(十三)ForkJoin14.JUC并发编程学习笔记(十四)异步回调15.JUC并发编程学习笔记(十五)JMM
16.JUC并发编程学习笔记(十七)彻底玩转单例模式
彻底玩转单例模式
单例中最重要的思想------->构造器私有!
恶汉式、懒汉式(DCL懒汉式!)
恶汉式
package single; //饿汉式单例(问题:因为一上来就把对象加载了,所以可能会导致浪费内存) public class Hungry { /* * 如果其中有大量的需要开辟的空间,如new byte[1024*1024]这些,那么一开始就会加载,而不是需要时才加载,所以非常浪费空间 * * */ private byte[] data1 = new byte[1024*1024]; private byte[] data2 = new byte[1024*1024]; private byte[] data3 = new byte[1024*1024]; private byte[] data4 = new byte[1024*1024]; private Hungry() { } private final static Hungry HUNGRY = new Hungry(); public static Hungry getInstance(){ return HUNGRY; } }
懒汉式
DCL懒汉式
完整的双重检测锁模式的单例、懒汉式、DCL懒汉式
package single; public class LazyMan { private LazyMan() { System.out.println(Thread.currentThread() + "ok"); } private volatile static LazyMan lazyMan; // 单线程下确实ok public static LazyMan getInstance() { // 加锁、锁整个类 // 双重检测锁模式的单例、懒汉式、DCL懒汉式 if (lazyMan==null){ synchronized (LazyMan.class){ if (lazyMan == null) { lazyMan = new LazyMan();//不是原子性操作 } } } return lazyMan; } /* * 1、分配内存空间 * 2、执行构造方法,初始化对象 * 3、把这个对象指向这个空间 * * 期望的结果:1、2、3 * 但是由于指令重排可能导致结果为1、3、2,这在cpu中是没问题的 * 线程A:1、3、2 * 线程B如果在线程A执行到3时开始执行判断是否为null,由于已经占用空间了,所以会被判断为不为空,但实际还未初始化对象,实际结果还是为null * * * */ // 多线程并发测试 public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(() -> { LazyMan.getInstance(); }).start(); } } }
但是有反射!只要有反射,任何的代码都不安全,任何的私有关键字都是摆设
正常的单例模式:
/* * 正常的单例模式创建的都为同一个对象,并且该对象全局唯一 * 只执行一次创建,并且对象都是同一个 * Thread[main,5,main]ok * true * */ LazyMan instance1 = LazyMan.getInstance(); LazyMan instance2 = LazyMan.getInstance(); System.out.println(instance2==instance1);
反射破坏单例:
/* * 通过反射破坏单例 * 执行两个创建,两个不同的对象 * Thread[main,5,main]ok Thread[main,5,main]ok false * */ LazyMan instance1 = LazyMan.getInstance(); Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor(); declaredConstructor.setAccessible(true); LazyMan instance2 = declaredConstructor.newInstance(); System.out.println(instance2 == instance1);
怎么去解决这种破坏呢?
首先反射走了无参构造器,我们可以在构造器中进行加锁判断是否已经存在了对象。
private LazyMan() { //通过构造器来加锁判断防止反射破坏 synchronized (LazyMan.class){ if (lazyMan!=null){ throw new RuntimeException("不要试图使用反射破坏单例模式"); } } }
通过反射破坏单例模式
道高一尺,魔高一丈
1、通过普通的反射来破坏单例模式
Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor(); declaredConstructor.setAccessible(true); LazyMan lazyMan1 = LazyMan.getInstance(); LazyMan lazyMan2 = declaredConstructor.newInstance(); System.out.println(lazyMan1); System.out.println(lazyMan2);
解决方法:通过构造器加锁解决
private LazyMan() { //通过构造器来加锁判断防止反射破坏 synchronized (LazyMan.class){ if (lazyMan == null){ }else { throw new RuntimeException("不要试图使用反射破坏单例模式"); } } }
2、通过反射创建两个类来破坏单例模式
Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor(); declaredConstructor.setAccessible(true); LazyMan lazyMan1 = declaredConstructor.newInstance(); LazyMan lazyMan2 = declaredConstructor.newInstance(); System.out.println(lazyMan1); System.out.println(lazyMan2);
解决方法:设置一个外部私有变量,在构造方法中通过外部私有变量来操作
//创建一个外部的标,用于防止通过newInstance破坏单例模式 private static boolean flg = true; private LazyMan() { //通过构造器来加锁判断防止反射破坏 synchronized (LazyMan.class){ if (flg){ flg = false; }else { throw new RuntimeException("不要试图使用反射破坏单例模式"); } } }
3、通过反射字段来将外部私有变量修改。
Constructor declaredConstructor = LazyMan.class.getDeclaredConstructor(); declaredConstructor.setAccessible(true); //通过反射修改内部私有变量 Field flg1 = LazyMan.class.getDeclaredField("flg"); flg1.setAccessible(true); //通过反射的newInstance创建的两个对象依旧破坏了单例模式 LazyMan instance1 = declaredConstructor.newInstance(); //通过反射字段对单例模式进行破坏 flg1.set(instance1,true); LazyMan instance2 = declaredConstructor.newInstance(); System.out.println(instance2 == instance1);
解决方法,通过枚举类型!枚举类型自带单例模式,禁止反射破坏
package single; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; //枚举类 public enum EnumDemo { INSTANCE; public EnumDemo getInstance(){ return INSTANCE; } } class EnumDemoTest{ public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Constructor declaredConstructor = EnumDemo.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); EnumDemo enumDemo1 = declaredConstructor.newInstance(); EnumDemo enumDemo2 = declaredConstructor.newInstance(); System.out.println(enumDemo1); System.out.println(enumDemo2); } }
发现抱错,没有对应的无参构造
但是idea编译的源码中是由无参构造的
idea欺骗了我们,那么编译好的类到底有没有无参构造,通过javap -p反编译源码查看所以方法
可以看到,也有空参的构造方法,也就意味了反编译源码也欺骗了你,所以我们通过更专业的工具来查看,使用jad查看。
查看当前目录新生成的java文件可以发现,通过jad反编译的源码的构造函数时个有参构造函数
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://www.kpdus.com/jad.html // Decompiler options: packimports(3) // Source File Name: EnumDemo.java package single; public final class EnumDemo extends Enum { public static EnumDemo[] values() { return (EnumDemo[])$VALUES.clone(); } public static EnumDemo valueOf(String name) { return (EnumDemo)Enum.valueOf(single/EnumDemo, name); } private EnumDemo(String s, int i) { super(s, i); } public EnumDemo getInstance() { return INSTANCE; } public static final EnumDemo INSTANCE; private static final EnumDemo $VALUES[]; static { INSTANCE = new EnumDemo("INSTANCE", 0); $VALUES = (new EnumDemo[] { INSTANCE }); } }
我们尝试在反射中加入这两个参数类
Constructor declaredConstructor = EnumDemo.class.getDeclaredConstructor(String.class,int.class);
可以发现,它根据我们预想的结果抛出一个异常
在newInstance方法中如果时枚举类就会抛出这个异常,这是从反射层面限制了对枚举类单例模式的破坏!!