JUC详解-17-单例模式- 四种实现方式&反射破坏及解决办法

JUC详解 -> 单例模式 - 四种实现方式&反射破坏及解决办法

Volatile --> 单例模式!

饿汉式 DCL懒汉式

枚举为什么可以避免单例模式被破坏?

1. 什么是单例模式
  • 当需要某个类只能被实例化一次的时候,就要使用单例模式。
  • 单例模式是比较简单的一种设计模式。
  • 一个类如何才能只被实例化一次? 一般实例化对象时,都会调用类的构造方法,因此将该类的构造方法private,就不能向外界提供,即保证了该类对象的唯一性。但这样的话,就无法调用这个类的构造方法了,也就无法实例化对象了,那么由谁来实例化对象呢?只能由类自身调用了。

代码测试

public class Singleton {
    //私有化构造方法
    private Singleton(){}
    //在类中定义getInstance()方法来提供一个该类的实例化对象
    //将这个方法表示为静态方法static就可以调用这个方法了
    public static Singleton getInstance(){
        return new Singleton();
    }
}
//这样就可以直接调用Singleton.getInstance()来创建对象
2. 饿汉式单例模式
//饿汉式单例模式
public class Hungry {
    //构造器私有
    private Hungry(){}
    
    //问题:假如代码中都还没用到这个类的实例化对象,如果这个类很复杂,可能会浪费空间
    //解决:最好是当要使用这个实例对象时再创建 --懒汉式
    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];

    //在类中事先创建好一个实例对象,每次调用getInstance()返回这个对象即可
    //只要这个类一加载完,就会创建实例对象。——饿汉式单例模式
    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance(){
        return HUNGRY;
    }
}
3. 懒汉式单例模式
  • 懒汉式单例模式:当使用这个实例对象时再创建;
  • 解决了上述饿汉式单例模式带来的资源浪费的问题。
  • 但是会存在问题:如创建10个线程,就不安全了,有时候会踹下你类被实例化了多次。
//懒汉式单例
public class LazyMan {
    private LazyMan(){        
        System.out.println(Thread.currentThread().getName()+" ok");
    }
    //只有当用到的时候才创建对象
    private static LazyMan lazyMan;

    public static LazyMan getInstance(){
        if(lazyMan == null){         //------------1
            lazyMan = new LazyMan(); //------------2
        }
        return lazyMan;
    }

    //问题:单线程下没有问题;
    //多线程并发情况下会有问题 : 不安全
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                lazyMan.getInstance();
            }).start();
        }
    }
}

LazyMan运行结果1

  • 为什么会出现这种问题?

    • 假如两个线程A和B;
    • 首先A调用了getInstance(),当执行到 if(LazyMan == null) 时,会判断对象是否为空,条件成立,就进入花括号中执行语句 LazyMan = new LazyMan();,若此时A突然停了,CPU切换执行线程B;
    • 当B执行getInstance()时,也会和A一样,先判断if(LazyMan == null)是否成立,但此时A线程停止了还没有创建实例,所以该条件还是成立,B也会进入花括号执行LazyMan = new LazyMan();,此时B顺利执行完,创建了一个实例并返回了;
    • 然后CPU又切换去执行A,此时A继续执行下面的语句 LazyMan = new LazyMan();,也创建了一个实例,并返回。
    • 因此,破坏了单例模式。
  • 那如何解决该问题?

    • ①在getInstance()前加关键字synchronized,即synchronized同步方法

      public class LazyMan {
          private LazyMan(){
              System.out.println(Thread.currentThread().getName()+" ok");
          }
          //只有当用到的时候才创建对象
          private static LazyMan lazyMan;
          
      	//synchronized关键字修饰getInstance(),即同步方法
          public static synchronized LazyMan getInstance(){
              if(lazyMan == null){
                  lazyMan = new LazyMan();
              }
              return lazyMan;
          }
      }
      //synchronized解决了该问题,但是synchronized会降低程序执行效率。
      
    • ②在LazyMan的类对象LazyMan.class前加关键字synchronized,synchronized同步代码块

      //DCL懒汉式单例模式
      public class LazyMan {
          private LazyMan(){
              System.out.println(Thread.currentThread().getName()+" ok");
          }
          //只有当用到的时候才创建对象
          private static LazyMan lazyMan;
      	//实例的重复创建问题本质还是在实例还没有被创建时,因此,只要实例创建成功了就没有必要加锁了。因此只要给实例还没创建时的if(lazyMan == null){lazyMan = new LazyMan();}语句块加锁即可,即同步代码块
          //双重检验锁模式(Double Checked Locking Pattern)
          public static LazyMan getInstance(){
              if(lazyMan == null){
                  synchronized(LazyMan.class){
                      if(lazyMan == null){
                          lazyMan = new LazyMan();
                      }
                  }
              }
              return lazyMan;
          }
      }
      //存在的问题:在极端情况下,由于lazyMan = new LazyMan()不是原子性操作,会出现指令重排的问题
      

      DCL懒汉式单例模式解决了多线程并发下重复创建实例的问题,同时也提高了代码的执行效率。

      但还存在问题!指令重排的问题!在极端情况下,由于lazyMan = new LazyMan()不是原子性操作,会出现指令重排的问题。

      那么如何解决?

      • 分析问题
      lazyMan = new LazyMan();
      /**
       * 不是原子性操作,大概概括为以下3步操作:
       * 1.在栈内存中创建lazyMan变量,在堆内存中开辟一块空间分配给LazyMan实例对象,该空间会得到一个随机地址假设为0x0001;
       * 2.执行构造方法,初始化LazyMan实例对象;
       * 3.将lazyMan变量指向该实例对象,即将该对象的地址0x0001赋值给lazyMan变量。
       */
      

      因此,CPU执行时,为了提高程序运行效率,会随机打乱部分指令执行的顺序,因此,对于lazyMan = new LazyMan();当CPU执行时,是无法保证一定是按照123执行的,也有可能以132执行。

      假如线程A以132的顺序执行;此时线程B执行getInstance(),会认为lazyMan!=null,就会直接return lazyMan,但此时lazyMan还没有完成构造!这时程序也就出现了问题,即指令重排问题。

      • 解决:用volatile关键词修饰lazyMan变量,可以解决指令重排问题!
      //DCL懒汉式单例模式 + volatile禁止指令重排
      
      public class LazyMan {
          private LazyMan(){
              System.out.println(Thread.currentThread().getName()+" ok");
          }
          //volatile可以解决指令重排问题
          private volatile static LazyMan lazyMan;
      
          public static LazyMan getInstance(){
              if(lazyMan == null){
                  synchronized(LazyMan.class){
                      if(lazyMan == null){
                          lazyMan = new LazyMan(); //不是原子性操作,会导致指令重排问题
                      }
                  }
              }
              return lazyMan;
          }
      }
      
      • 为什么volatile可以解决这个问题?
      • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作 !
4. 静态内部类实现单例模式
//静态内部类实现单例模式
public class Holder {

    private  Holder(){}

    private static Holder getInstance(){
        return InnerClass.HOLDER;
    }

    public static class InnerClass{
        private static final Holder HOLDER = new Holder();
    }
}
  • 但是这都不安全!因为反射!
5. 反射破坏单例模式

第1种破坏:
import java.lang.reflect.Constructor;

public class LazyMan {
    private LazyMan(){}
    private volatile static LazyMan lazyMan;

    public static LazyMan getInstance(){
        if(lazyMan == null){
            synchronized(LazyMan.class){
                if(lazyMan == null){
                    lazyMan = new LazyMan();                   
                }
            }
        }
        return lazyMan;
    }
    
    //反射破坏单例模式!
    public static void main(String[] args) throws Exception {
        LazyMan instance1 = LazyMan.getInstance();
        //利用反射创建对象instance2,破坏了单例模式
        Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null);
        lazyManConstructor.setAccessible(true); //无视了private构造器
        LazyMan instance2 = lazyManConstructor.newInstance(); 
        System.out.println(instance1);
        System.out.println(instance2);
    }
}
  • 运行结果:
com.anobabe.single.LazyMan@1b6d3586
com.anobabe.single.LazyMan@4554617c
  • 解决:通过在构造方法种加锁,防止这种反射破坏!
import java.lang.reflect.Constructor;

public class LazyMan {
    private LazyMan(){
        synchronized (LazyMan.class){
            if(lazyMan != null){
                throw new RuntimeException("不要试图使用反射破坏异常");
            }
        }
    }

    private volatile static LazyMan lazyMan;

    public static LazyMan getInstance(){
        if(lazyMan == null){
            synchronized(LazyMan.class){
                if(lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }

        }
        return lazyMan;
    }
    //反射!
    public static void main(String[] args) throws Exception {
        LazyMan instance1 = LazyMan.getInstance();
        //利用反射创建对象instance2,破坏了单例模式
        Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null);
        lazyManConstructor.setAccessible(true); //无视了private构造器
        LazyMan instance2 = lazyManConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}
  • 运行结果
...
Caused by: java.lang.RuntimeException: 不要试图使用反射破坏异常
	at com.anobabe.single.LazyMan.<init>(LazyMan.java:11)
	... 5 more
第2种破坏:
  • 两个实例都通过反射来new!
import java.lang.reflect.Constructor;

public class LazyMan {
    private LazyMan(){
        synchronized (LazyMan.class){
            if(lazyMan != null){
                throw new RuntimeException("不要试图使用反射破坏异常");
            }
        }
    }

    private volatile static LazyMan lazyMan;

    public static LazyMan getInstance(){
        if(lazyMan == null){
            synchronized(LazyMan.class){
                if(lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
    //反射!
    public static void main(String[] args) throws Exception {
        //利用反射同时创建对象instance1、instance2,破坏单例模式
        Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null);
        lazyManConstructor.setAccessible(true); //无视了private构造器
        LazyMan instance1 = lazyManConstructor.newInstance();
        LazyMan instance2 = lazyManConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}
  • 运行结果
com.anobabe.single.LazyMan@1b6d3586
com.anobabe.single.LazyMan@4554617c
  • 解决:利用红绿灯防止这种反射的破坏!
package com.anobabe.single;
//懒汉式单例

import java.lang.reflect.Constructor;


public class LazyMan {
	//红绿灯
    private static boolean anoBabe = false;

    private LazyMan(){
        synchronized (LazyMan.class){
            if(anoBabe == false){
                anoBabe = true;
            }else{
                throw new RuntimeException("不要试图使用反射破坏异常");
            }
            
        }
    }

    private volatile static LazyMan lazyMan;

    public static LazyMan getInstance(){
        if(lazyMan == null){
            synchronized(LazyMan.class){
                if(lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
    //反射!
    public static void main(String[] args) throws Exception {
        //利用反射同时创建对象instance1、instance2,破坏单例模式
        Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null);
        lazyManConstructor.setAccessible(true); //无视了private构造器
        LazyMan instance1 = lazyManConstructor.newInstance();
        LazyMan instance2 = lazyManConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}
  • 运行结果:
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at com.anobabe.single.LazyMan.main(LazyMan.java:50)
Caused by: java.lang.RuntimeException: 不要试图使用反射破坏异常
	at com.anobabe.single.LazyMan.<init>(LazyMan.java:17)
	... 5 more
第3种破坏:
  • 通过反编译,假设知道了变量anoBabe
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class LazyMan {

    private static boolean anoBabe = false;

    private LazyMan(){

        synchronized (LazyMan.class){
            if(anoBabe == false){
                anoBabe = true;
            }else{
                throw new RuntimeException("不要试图使用反射破坏异常");
            }
        }

    private volatile static LazyMan lazyMan;

    public static LazyMan getInstance(){
        if(lazyMan == null){
            synchronized(LazyMan.class){
                if(lazyMan == null){
                    lazyMan = new LazyMan();     
                }
            }
        }
        return lazyMan;
    }
    //反射!
    public static void main(String[] args) throws Exception {
        Field anoBabe = LazyMan.class.getDeclaredField("anoBabe");
        anoBabe.setAccessible(true);
        
        Constructor<LazyMan> lazyManConstructor = LazyMan.class.getDeclaredConstructor(null);
        lazyManConstructor.setAccessible(true); //无视了private构造器
        LazyMan instance1 = lazyManConstructor.newInstance();
        
        anoBabe.set(instance1,false);

        LazyMan instance2 = lazyManConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }               
}
  • 运行结果:
com.anobabe.single.LazyMan@4554617c
com.anobabe.single.LazyMan@74a14482

道高一尺魔高一丈!

那么到底如何解决呢?

  • newInstance源码:
    Constructor下的newInstance源码
6. 枚举类实现单例模式
//枚举 enum :本身就是一个类
public enum EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

class Test{
    public static void main(String[] args) {
        EnumSingleton instance1 = EnumSingleton.INSTANCE;
        EnumSingleton instance2 = EnumSingleton.INSTANCE;
        System.out.println(instance1);
        System.out.println(instance2);
    }
}
  • 运行结果:
INSTANCE
INSTANCE

尝试破坏!

  • 先分析该枚举类单例模式源码是有参构造还是无参构造?

IDEA中EnumSingleton源码

import java.lang.reflect.Constructor;
//枚举类单例模式!
//枚举 enum :本身就是一个类
public enum EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

class Test{
    public static void main(String[] args) throws Exception {
        EnumSingleton instance1 = EnumSingleton.INSTANCE;
        //EnumSingleton instance2 = EnumSingleton.INSTANCE;
        //通过反射创建instance2
        Constructor<EnumSingleton> enumSingletonConstructor = EnumSingleton.class.getDeclaredConstructor(null);
        enumSingletonConstructor.setAccessible(true);
        EnumSingleton instance2 = enumSingletonConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}
//java.lang.NoSuchMethodException: com.anobabe.single.EnumSingleton.<init>()
  • 运行结果:不是我们想要的"Cannot reflectively create enum objects"
Exception in thread "main" java.lang.NoSuchMethodException: com.anobabe.single.EnumSingleton.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.anobabe.single.Test.main(EnumSingleton.java:17)
  • 通过javap反编译,还是可以看到有该无参构造啊!

javap-EnumSingleton

  • 用jad 反编译生成java文件
    jad-sjavajad-EnumSingleton
  • 所以,应该将原来的无参改成有参构造:
import java.lang.reflect.Constructor;
public enum EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}
class Test{
    public static void main(String[] args) throws Exception {
        EnumSingleton instance1 = EnumSingleton.INSTANCE;
        //用有参构造!
        Constructor<EnumSingleton> enumSingletonConstructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
        enumSingletonConstructor.setAccessible(true);
        EnumSingleton instance2 = enumSingletonConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(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.anobabe.single.Test.main(EnumSingleton.java:19)

枚举类单例模式确实不能被反射破坏!!!

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值