Android开发经验谈Android技术知识Android开发

你应该了解的单例模式

2019-11-05  本文已影响0人  闲庭

单元素的枚举类型经常成为实现 Singleton 的最佳方法 。

什么是单例?就一条基本原则,单例对象的类只会被初始化一次。在 Java 中,我们可以说在 JVM 中只存在该类的唯一一个对象实例。在 Android 中,我们可以说在程序运行期间,该类有且仅有一个对象实例。

单例模式的简单实现步骤:

  1. 构造方法私有,保证无法从外部通过 new 的方式创建对象。
  2. 对外提供获取该类实例的静态方法。
  3. 类的内部创建该类的对象,通过第 2 步的静态方法返回。

按照上述步骤写下你认为比较严谨的单例模式,然后看看你所写下的单例能否满足以下条件:

如果你的单例已满足上述条件,给自己鼓个掌,如不满足上述条件或者有兴趣的可接着了解下,主要从下面几种创建单例的方式描述对应实现方式的优缺点以及如何优化:


一、饿汉式

public class SingleTon {
    //第三步创建唯一实例
    private static SingleTon instance = new SingleTon();
    
    //第一步构造方法私有
    private SingleTon() {
    }
    
    //第二步暴露静态方法返回唯一实例
    public static SingleTon getInstance() {
        return instance;
    } 
}

优点:
设计简单 ,解决了多线程实例化的问题。

缺点:
在虚拟机加载SingleTon类的时候,将会在初始化阶段为类静态变量赋值,也就是在虚拟机加载该类的时候(此时可能并没有调用 getInstance 方法)就已经调用了 new SingleTon(); 创建了该对象的实例,之后不管这个实例对象用不用,都会占据内存空间。

二 、懒汉式

public class SingleTon {
    //创建唯一实例
    private static SingleTon instance = null;
    
    private SingleTon() {
    }
    
    public static SingleTon getInstance() {
        //延迟初始化 在第一次调用 getInstance 的时候创建对象
        if (instance == null) {
            instance = new SingleTon();
        }
        return instance;
    } 
}

优点:
设计也是比较简单的,和饿汉式不同,当这个Singleton被加载的时候,被static修饰的静态变量将会被初始化为null,这个时候并不会占用内存,而是当第一次调用getInstance方法的时候才会被初始化实例对象,按需创建。

缺点:
在单线程环境下是没有问题的,在多线程环境下,会产生线程安全问题。在有两个线程同时 运行到了 instane == null这个语句,并且都通过了,那他们就会都各自实例化一个对象,这样就又不是单例了。

如何解决懒汉式在多线程环境下的多实例问题?

三、枚举实现单例

public enum SingletonEnum {
    INSTANCE;

    public static void main(String[] args) {
        System.out.println(SingletonEnum.INSTANCE == SingletonEnum.INSTANCE);
    }
}

枚举实现单例是最为推荐的一种方法,因为就算通过序列化,反射等也没办法破坏单例性。(关于Android使用枚举会产生性能问题的说法,这应该是Android 2.x系统之前内存紧张的时代了,现在已经Android Q了,相信某些场合枚举所带来的便利远远大于这点所谓的性能影响)

四. 如何避免单例模式反射攻击

以最初的DCL为测试案例,看看如何进行反射攻击及又如何在一定程度上避免反射攻击。
反射攻击代码如下:

 public static void main(String[] args) {

     SingleTon singleton1 = SingleTon.getInstance();
     SingleTon singleton2 = null;

     try {
         Class<SingleTon> clazz = SingleTon.class;
         Constructor<SingleTon> constructor = clazz.getDeclaredConstructor();
         constructor.setAccessible(true);
         singleton2 = constructor.newInstance();
     } catch (Exception e) {
         e.printStackTrace();
     }

     System.out.println("singleton1.hashCode():" + singleton1.hashCode());
     System.out.println("singleton2.hashCode():" + singleton2.hashCode());
 }

执行结果:

 singleton1.hashCode():1296064247
 singleton2.hashCode():1637070917

通过执行结果发现通过反射破坏了单例。
如何保证反射安全呢?只能以暴制暴,当已经存在实例的时候再去调用构造函数直接抛出异常,对构造函数做如下修改:

  public class SingleTon {
     //创建唯一实例
     private static volatile SingleTon instance = null;
   
     private SingleTon() {
         if (instance != null) {
             throw new RuntimeException("单例构造器禁止反射调用");
         }
     }
   
     public static SingleTon getInstance() {
         if (instance == null) {
           synchronized (SingleTon.class) {   
               if (instance == null) {
                   instance = new SingleTon();
               }
           }
       }
       return instance;
     } 
 }

此时可防御反射攻击,抛出异常如下:

 java.lang.reflect.InvocationTargetException
 at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
 at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
 at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
 at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
 at com.liujc.demo.TestUtil.testSingleInstance(TestUtil.java:45)
 at com.liujc.demo.TestUtil.main(TestUtil.java:33)
 Caused by: java.lang.RuntimeException: 单例构造器禁止反射调用
 at com.liujc.demo.SingleTon.<init>(SingleTon.java:16)
 ... 6 more
 Exception in thread "main" java.lang.NullPointerException
 at com.liujc.demo.TestUtil.testSingleInstance(TestUtil.java:49)
 at com.liujc.demo.TestUtil.main(TestUtil.java:33) 
 Process finished with exit code 1

然后我们把上述测试代码修改如下(调换了singleton1的初始化顺序)

 public static void main(String[] args) {
     SingleTon singleton2 = null;

     try {
         Class<SingleTon> clazz = SingleTon.class;
         Constructor<SingleTon> constructor = clazz.getDeclaredConstructor();
         constructor.setAccessible(true);
         singleton2 = constructor.newInstance();
     } catch (Exception e) {
         e.printStackTrace();
     }

     System.out.println("singleton2.hashCode():" + singleton2.hashCode());

     SingleTon singleton1 = SingleTon.getInstance(); //调换了位置,在反射之后执行
     System.out.println("singleton1.hashCode():" + singleton1.hashCode());
 }

执行结果:

 singleton2.hashCode():1296064247
 singleton1.hashCode():1637070917

发现此防御未起到作用。

缺点:

如何避免序列化攻击?
只需要修改反序列化的逻辑就可以了,即重写 readResolve() 方法,使其返回统一实例。

   protected Object readResolve() {
       return getInstance();
   }

脆弱不堪的单例模式经过重重考验,进化成了完全体,延迟加载,线程安全,反射及序列化安全。简易代码如下:


(枚举实现单例是最为推荐的一种方法,因为就算通过序列化,反射等也没办法破坏单例性,底层实现比如newInstance方法内部判断枚举抛异常)

上一篇下一篇

猜你喜欢

热点阅读