设计模式之单例模式最佳实现方式

  • 单例模式是什么?

    对象在全局只能有一个实例

  • 为什么要使用单例模式?

    • 静态方法和非静态方法的区别?

      • 静态的方法:

        能够在它的类的任何对象创建之前被访问,而不必引用任何对象,

        并且static修饰的属性和方法在整个类中只有一份,可共享,放在方法区中。

      • 非静态的方法:

        在创建实例对象时,因为属性的值对于每个对象都各不相同,

        因此在new一个实例时,栈中有个引用地址指向了 ---> 堆中new出来的实例化对象,里面包含对象独有的属性和方法,

        再次new实例化有不同属性值的该对象时,会重新创建一个栈中的引用地址指向 ---> 堆中new出来的实例化对象。

    • 为什么要使用单例模式而非静态方法?

      • 从面向对象的方式:

        静态方法是基于对象,单例模式是面向对象,

        如果一个方法不受限于它存在类的实例对象,那么这个方法应该是静态的,如果确实要使用非静态的方法,但是只想维持一份实例化对象,只有一个对象可以访问该方法,那么就需要使用单例模式,

        而且,如果需要在系统运行的时候就加载一些在整个类的生命周期都存在的属性和方法,那这些类最好是独一份并且可以共享的,否则new实例化对象时再重新赋值毫无意义且浪费内存,所以需要用静态方法或单例模式来维持这些独一份并且可以共享的属性和方法,静态方法虽然能同样解决问题,但是最好的解决方案应该是面向对象的单例模式。

      • 从功能上:

        静态方法和单例模式都能保证独一份,

        但是单例模式可以控制单例数量,进行有意义的派生,对实例的创建有更自由的控制。

  • 怎样实现单例?

    • 类的构造方法私有化,保证其他类不能实例化该对象

    • 本类中实例化该对象,保证该类在全局中存在

    • 创建一个公有的方法,返回的结果是实例化的该对象,供别人使用

1、饿汉式单例

  • 在类创建的时候就直接初始化该类

//代码实现
public class HungryMan {

   private HungryMan(){}

   /*
   static:
  HUNGRY_MAN需要在调用getInstance()之前就已经被初始化了,只有static的成员才能在没有创建对象时进行初始化。
  类的static成员在类第一次被使用时初始化后就不会再被初始化,保证了单例。
 
   final:
  保证必须给 HungryMan HUNGRY_MAN 赋值,也就是必须实例化该类
   */
   private static final HungryMan HUNGRY_MAN = new HungryMan();

   public static HungryMan getInstance(){
       return HUNGRY_MAN;
  }
}
  • 在多线程中使用饿汉式单例,8个线程只有一个线程能使用该类的构造方法,说明饿汉式单例是线程安全的

public class HungryMan {
   private HungryMan(){
       System.out.println("饿汉式单例" + Thread.currentThread().getName());
  }
   
   private static final HungryMan HUNGRY_MAN = new HungryMan();
   
   public static HungryMan getInstance(){
       return HUNGRY_MAN;
  }
   
   public static void main(String[] args){
       for (int i = 0; i <9 ; i++) {
           new Thread(()->{
               HungryMan.getInstance();
          }).start();
      }
  }
}

//输出结果:饿汉式单例 Thread-0
  • 优点:

    线程安全。

  • 缺点:

    该类进入JVM时,不论该类会不会被使用就直接被实例化,浪费了内存空间。

2、懒汉式单例

  • 使用该类的时候才初始化该类

//代码实现
public class LazyMan {

   private LazyMan(){}

   private static LazyMan lazyMan;
   
   public static LazyMan getInstance(){
       if (lazyMan == null){
           lazyMan = new LazyMan();
      }
       return lazyMan;
  }
}
  • 在多线程下使用饿汉式单例,8个线程可能有多个线程能使用该类的构造方法,说明懒汉式单例是线程不安全的

public class LazyMan {

   private LazyMan(){
       System.out.println("懒汉式单例 " + Thread.currentThread().getName());
  }

   private static LazyMan lazyMan;
   
   public static LazyMan getInstance(){
       if (lazyMan == null){
           lazyMan = new LazyMan();
      }
       return lazyMan;
  }

   public static void main(String[] args) {
       for (int i = 0; i <9 ; i++) {
           new Thread(()->{
               LazyMan.getInstance();
          }).start();
      }
  }
}

/*
输出结果:
懒汉式单例 Thread-0
懒汉式单例 Thread-2
懒汉式单例 Thread-1
*/
  • 优点

    节省内存空间

  • 缺点:

    线程不安全

3、DCL懒汉式(双重检查懒汉式)

  • 解决懒汉式线程不安全的问题

//代码实现
public class DCLLazyMan {

   private DCLLazyMan(){}

   /*
   volatile:
  在原子性上来说是不安全的:
new DCLLazyMan(),创建对象的实例会分解成以下3个指令
•1、分配内存空间
•2、执行构造方法,初始化对象
•3、对象指向内存空间
我们期望按1 2 3 的顺序依次完成,但是在内存中会出现指令重排的过程:
假设A类new DCLLazyMan(),是先分配内存空间、占用此空间、初始化对象,也就是132的顺序执行,
同时另一个线程中的B类也在new DCLLazyMan(),此时A还没有完成DCLLazyMan的构造,就会出现不安全的问题。

必须加上volatile关键字!
   */
   private volatile static DCLLazyMan lazyMan;

   public static DCLLazyMan getInstance(){
       //第一重检查
       if (lazyMan == null){
           //加锁
           synchronized (DCLLazyMan.class){
               //第二重检查
               if (lazyMan == null){
                   lazyMan = new DCLLazyMan();
              }
          }
      }
       return lazyMan;
  }
}
  • 在多线程中使用DCL懒汉式,8个线程只有一个线程能使用该类的构造方法,说明DCL懒汉式单例是线程安全的

public class DCLLazyMan {

   private DCLLazyMan(){
       System.out.println("DCL懒汉式单例 " + Thread.currentThread().getName());
  }

   private volatile static DCLLazyMan lazyMan;

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

   public static void main(String[] args) {
       for (int i = 0; i <9 ; i++) {
           new Thread(()->{
               DCLLazyMan.getInstance();
          }).start();
      }
  }
}

//输出结果:DCL懒汉式单例 Thread-0
  • 使用反射破解DCL懒汉式单例的唯一性

public static void main(String[] args) throws Exception {

   //创建对象1和2,通过DCLLazyMan.getInstance()方法
   DCLLazyMan instance1 = DCLLazyMan.getInstance();
   DCLLazyMan instance2 = DCLLazyMan.getInstance();

   //创建对象3,通过反射获得构造方法的newInstance()方法
   Constructor constructor = DCLLazyMan.class.getDeclaredConstructor(null);
   constructor.setAccessible(true);
   DCLLazyMan instance3 = (DCLLazyMan)constructor.newInstance();

   //输出结果:instance1和instance2相等吗? true
   System.out.println("instance1和instance2相等吗? " + (instance1 == instance2));
   //输出结果:instance2和instance3相等吗? false
   System.out.println("instance2和instance3相等吗? " + (instance2 == instance3));
}

/*
结果分析:
对象1和2,都是通过正常方式创建的,所以指向的都是同一个对象
对象2和3,一个通过正常方式创建,一个通过反射方式创建,已经不是同一个对象了

说明反射可以破坏DCL懒汉式单例的唯一性
*/

4、解决DCL懒汉式被反射破坏唯一性的问题

4.1、第一种方法:再加一重检查

//修改TCLLazyMan的构造方法为:
private TCLLazyMan(){
   //第三重加锁判断
   synchronized (TCLLazyMan.class){
       if (lazyMan != null){
           throw new RuntimeException("不要使用反射破坏类的唯一性");
      }
  }
}


//测试
public static void main(String[] args) throws Exception {

   //创建对象1,通过DCLLazyMan.getInstance()方法
   TCLLazyMan instance1 = TCLLazyMan.getInstance();

   //创建对象2,通过反射获得构造方法的newInstance()方法
   Constructor constructor = TCLLazyMan.class.getDeclaredConstructor(null);
   constructor.setAccessible(true);
   TCLLazyMan instance2 = (TCLLazyMan)constructor.newInstance();
}

/*
结果分析:
Exception in thread "main" java.lang.reflect.InvocationTargetException
Caused by: java.lang.RuntimeException: 不要使用反射破坏类的唯一性

说明三重检测确实可以防止在对象已经被实例化后,再通过反射来创建对象的实例化
*/

但是如果两个对象均由反射方法创建呢?

public static void main(String[] args) throws Exception {

   //创建对象1和2,通过反射获得构造方法的newInstance()方法
   Constructor constructor = TCLLazyMan.class.getDeclaredConstructor(null);
   constructor.setAccessible(true);
   TCLLazyMan instance1 = (TCLLazyMan)constructor.newInstance();
   TCLLazyMan instance2 = (TCLLazyMan)constructor.newInstance();

   //输出结果:instance1和instance2相等吗? false
   System.out.println("instance1和instance2相等吗? " + (instance1 == instance2));
}

/*
结果分析:
三重检测防止不了反射破坏类的唯一性
*/

经过代码分析:三重检测防止不了反射破坏类的唯一性

4.2、第二种办法:红绿灯标识

//修改TCLLazyMan的构造方法为:
private TCLLazyMan(){
   synchronized (TCLLazyMan.class){
       //标识为false,说明之前没有使用TCLLazyMan的构造方法,也就是没有被实例化过
       if (ahfndsjbvnc == false){
           //改变标识
           ahfndsjbvnc = true;
      }else{
           throw new RuntimeException("不要使用反射破坏类的唯一性");
      }
  }
}

//测试
public static void main(String[] args) throws Exception {

   //创建对象1和2,通过反射获得构造方法的newInstance()方法
   Constructor constructor = TCLLazyMan.class.getDeclaredConstructor(null);
   constructor.setAccessible(true);
   TCLLazyMan instance1 = (TCLLazyMan)constructor.newInstance();
   TCLLazyMan instance2 = (TCLLazyMan)constructor.newInstance();
}

/*
结果分析:
Exception in thread "main" java.lang.reflect.InvocationTargetException
Caused by: java.lang.RuntimeException: 不要使用反射破坏类的唯一性

说明这种方法好像可以防止反射创建不同对象
*/

但是如果通过反编译找到了设置标识的参数,并且修改了它的值呢?

public static void main(String[] args) throws Exception {

   //创建对象1和2,通过反射获得构造方法的newInstance()方法
   Constructor constructor = TCLLazyMan.class.getDeclaredConstructor(null);
   constructor.setAccessible(true);

   //通过反射获得设置标识的属性
   Field field = TCLLazyMan.class.getDeclaredField("ahfndsjbvnc");
   field.setAccessible(true);

   TCLLazyMan instance1 = (TCLLazyMan)constructor.newInstance();
   
   //改变属性的值
   field.set(instance1,false);

   TCLLazyMan instance2 = (TCLLazyMan)constructor.newInstance();

   ////输出结果:instance1和instance2相等吗? false
   System.out.println("instance1和instance2相等吗? " + (instance1 == instance2));
}

/*
结果分析:
设置红绿灯标识防止不了反射破坏类的唯一性
*/

经过代码分析:设置红绿灯标识防止不了反射破坏类的唯一性

4.3、第三种方法:通过枚举类型

  • 进入newInstance()的源码

    可知:通过枚举类型可以防止反射破坏单例模式的唯一性

public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException,
    IllegalArgumentException, InvocationTargetException
{
   if (!override) {
       if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
           Class<?> caller = Reflection.getCallerClass();
           checkAccess(caller, clazz, null, modifiers);
      }
  }
   if ((clazz.getModifiers() & Modifier.ENUM) != 0)
       //如果是枚举类型,会抛出异常
       throw new IllegalArgumentException("Cannot reflectively create enum objects");
   ConstructorAccessor ca = constructorAccessor;   // read volatile
   if (ca == null) {
       ca = acquireConstructorAccessor();
  }
   @SuppressWarnings("unchecked")
   T inst = (T) ca.newInstance(initargs);
   return inst;
}
  • 测试:

    • 创建一个枚举类

      public enum EnumSingle {

         INSTANCE;

         public EnumSingle getInstance(){
             return INSTANCE;
        }
      }
    • 通过反射创建对象

      public static void main(String[] args) throws Exception {

         Constructor constructor = EnumSingle.class.getDeclaredConstructor(null);
         constructor.setAccessible(true);
         EnumSingle instance = (EnumSingle)constructor.newInstance();

      }

      /*
      java.lang.NoSuchMethodException: com.hmx.EnumSingle.<init>()
      EnumSingle没有空的构造函数
      */
    • 分析:

      确实不可以通过反射创建对象,但是所报错误并不是源码中抛出的异常,为什么?

  • 分析所报错误并不是源码中抛出的异常的原因

    第一步:查看target中的class文件:

    public enum EnumSingle {
       INSTANCE;

       //class文件中存在无参构造方法
       private EnumSingle() {
      }

       public EnumSingle getInstance() {
           return INSTANCE;
      }
    }

    第二步:通过cmd命令行编译class文件:

    进入EnumSingle.class所在位置,输入javap -p EnumSingle.class命令

    Compiled from "EnumSingle.java"

    public final class com.hmx.EnumSingle extends java.lang.Enum<com.hmx.EnumSingle> {

     public static final com.hmx.EnumSingle INSTANCE;
     private static final com.hmx.EnumSingle[] $VALUES;

     public static com.hmx.EnumSingle[] values();
     public static com.hmx.EnumSingle valueOf(java.lang.String);

     //文件中同样存在无参构造方法
     private com.hmx.EnumSingle();
     public com.hmx.EnumSingle getInstance();
     static {};
    }

    第三步:使用jad工具把class文件反编译成Java文件:

    进入EnumSingle.class所在位置,输入jad -sjava EnumSingle.class命令

    发现EnumSingle类中存在有参构造方法

修改创建对象的代码

public static void main(String[] args) throws Exception {

   Constructor constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
   constructor.setAccessible(true);
   EnumSingle instance = (EnumSingle)constructor.newInstance();

}

/*
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)

不能通过反射创建枚举单例类型的对象
*/

总结

Enum实现单例模式是最佳的方法!

posted @ 2020-05-09 14:52  YoungDeng  阅读(939)  评论(0编辑  收藏  举报