彻底玩转单例模式
单例中最重要的思想------->构造器私有!
恶汉式、懒汉式(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<LazyMan> 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<LazyMan> 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<LazyMan> 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<LazyMan> 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<EnumDemo> 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<EnumDemo> declaredConstructor = EnumDemo.class.getDeclaredConstructor(String.class,int.class);
复制代码 可以发现,它根据我们预想的结果抛出一个异常

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

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