Android面试题--设计模式之单例模式的最全解析及六种实现方式 保证线程安全

本文深入解析单例模式的定义、实现思路及优缺点,并详细介绍了饿汉式、懒汉式等多种实现方式。此外,还探讨了单例模式在Android中的应用及可能导致的内存泄漏问题。

定义

单例模式是一种对象创建模式,它用于产生一个对象的具体实例,它可以确保系统中一个类只有一个实例

好处

  • 对于频繁使用的对象,可以省略多次创建对象所花费的时间,对于那些重量级对象而言,可以节省非常可观的性能开销
  • 由于new操作的次数减少,因而对系统内存的使用频率降低,减轻GC压力,缩短GC停顿时间,提高系统运行效率

实现思路

单例模式的实现思路基本一致,主要是两点:

  1. 将该类的构造方法使用private修饰,将实例化代码留在类内部
  2. 暴露一个公开方法提供给使用者获取该实例

至于该对象的实例化就看单例模式的写法了

使用场景

  • 需要频繁创建的对象

  • 创建次数少但是每次创建耗时过多或者耗费资源过多且经常用到的对象

例如一些工具类,使用非常频繁,可以考虑使用单例;还有数据库,网络请求等比较笨重的对象等

分类

饿汉式和懒汉式单例应该是大家接触的最多的了,其实单例模式的写法有多种,下面一一介绍

饿汉式

public class SingleTon {

    private static SingleTon singleTon = new SingleTon();

    private SingleTon(){ }

    public static SingleTon getInstance(){
        return singleTon;
    }
    
}

优点:写法简单,由于实例化代码是static修饰的,所以该类在被虚拟机加载的时候就会完成实例化,避免线程同步问题,而且后续在使用时无需等待实例化,效率高
缺点:由于在类加载的时候就会初始化,如果此时内部需要初始化大量数据,将会增加APP启动时间;同时如果后续很长时间内没有使用该变量或者就没有使用,将会造成系统内存浪费

懒汉式

    private static SingleTon singleTon;

    private SingleTon(){}

    public static SingleTon getInstance(){
        if (singleTon == null) {
            singleTon = new SingleTon();
        }
        return singleTon;
    }

优点:相比于饿汉式,它将类的初始化放到了开发者调用时,这起到了延迟加载的效果,如同它的名字一样(懒),这样使应用启动更快,某些情况下也能避免内存浪费
缺点:这种模式只能在单线程下使用,如果是多线程情况下,该模式将失效,无法保证实例全局唯一;比如线程A调用getInstance方法,第一次singleTon为null,进入到if判断里,但还未完成初始化,这时线程B也进入到了这个方法
因为singleTon此时还是null,所以它也需要进行初始化,这就导致最终singleTon的实例会出现多个;这将会导致严重的数据安全

线程安全懒汉式

    private static SingleTon singleTon;

    private SingleTon(){}

    public static synchronized SingleTon getInstance(){
        if (singleTon == null) {
            singleTon = new SingleTon();
        }
        return singleTon;
    }

优点:通过synchronized关键字实现线程安全,这样使得该对象只实例化一次
缺点:加锁意味着每个线程在访问该方法时都要去获取锁,将会降低访问效率

这与同步代码块是一样效果

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

DCL(双重检查锁机制)

    private static SingleTon singleTon;

    private SingleTon(){}

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

上面说了返回对象这个操作耗时是很小的,但synchronized对方法加锁导致每次访问时有大部分时间花在了同步上,有很大的性能开销,同步代码块同理;但是DCL(Double Check Lock)可以解决这个问题,因为只有第一次创建实例的时候才会进行同步,后面再访问时直接在最外层if判断对象不为null,就无需同步处理

但是这样写是否就完美了呢?很遗憾,并不是,那问题出在了哪里呢?

原因就是singleTon = new SingleTon();这句代码,你在编译器里看只有一句代码,但是虚拟机在执行的时候是执行相应的指令,当你反编译class文件后你就会见到如下字节码指令

0: aload_0 
1: new     
4: dup     
5: invokespecial 
8: putfield   
  • aload_0:这个指令是将singleTon这个变量进行入栈操作

  • new:这个指令的作用是创建新的对象实例

  • dup:复制之前分配的SingleTon空间的引用并压入栈顶

  • putfield:这个指令是将dup指令获取到的引用赋值给aload_0指令入栈的singleTon这个变量

简单理解为:

  1. 给singleTon实例分配内存
  2. 初始化SingleTon的构造器
  3. 将singleTon对象指向分配的内存空间

如果虚拟机就按照这样的顺序执行那就没有问题,但是由于JIT(即时编译技术,作用优化指令,提高程序运行效率,允许指令重排序)的存在,这样程序真正运行的时候指令顺序可能是这样的

  1. 给singleTon实例分配内存
  2. 将singleTon对象指向分配的内存空间
  3. 初始化SingleTon的构造器

这时候操蛋的时刻来了,线程A执行完第二步,正准备执行第三步时,线程B来了,它判断singleTon变量已经不是null,那就直接返回该变量;这样只要使用该变量就报异常了,因为虽然内存分配了,但是对象尚未初始化

synchronized关键字虽然保证了线程的原子性,即这部分代码要么全部执行,要么全不执行;但是单条语句编译后形成的指令并不是一个原子操作,就像上面说的,指令重排后线程A还没执行完全部指令,线程B跑来执行了

所以解决这个问题就需要禁止指令重排序,那我们就可以使用volatile关键字,即只需要给singleTon变量加上关键字

private static volatile SingleTon singleTon;

静态内部类

    private SingleTon() {}

    private static class SingleTonInsance{
        private static final SingleTon INSTANCE = new SingleTon();
    }

    public static SingleTon getInstance() {
        return SingleTonInsance.INSTANCE;
    }

这种写法跟饿汉式很像,两者都是采用了类加载的机制来保证初始化后只有一个实例,但是不同的是饿汉式中该类只要被加载就会被实例化,但是该模式下只有调用getInstance()方法时才会去完成SingleTon的实例化,起到了延迟加载的效果

但是它是如何保证线程安全的呢?要知道类的静态变量是唯一的,且由final修饰,即值一旦确定就不能被修改;同时该内部类的静态常量第一次被调用时,该内部类才会被加载,虚拟机会保证一个类的构造器< init>方法在多线程环境中被正确地加载,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的构造器< init>方法,其他线程都需要阻塞等待,直到活动线程执行< init>方法完毕

这种写法也是值得推荐的,有延迟加载,线程安全,效率高,写法简单的优点

枚举

public enum  SingleTon {
    INSTANCE
}

因为枚举是JDK1.5中新增的特性,可能实际开发中你见到这么写的会比较少

枚举的写法可以说是最简单的一种,创建枚举类的线程安全是由虚拟机保证的,通过反编译它的字节码可以看到

Compiled from "SingleTon.java"
public final class com.mango.newdemo.SingleTon extends java.lang.Enum<com.mango.newdemo.SingleTon> {
  public static final com.mango.newdemo.SingleTon INSTANCE;

  public static com.mango.newdemo.SingleTon[] values();
    Code:
       0: getstatic     #1                  // Field $VALUES:[Lcom/mango/newdemo/SingleTon;
       3: invokevirtual #2                  // Method "[Lcom/mango/newdemo/SingleTon;".clone:()Ljava/lang/Object;
       6: checkcast     #3                  // class "[Lcom/mango/newdemo/SingleTon;"
       9: areturn

  public static com.mango.newdemo.SingleTon valueOf(java.lang.String);
    Code:
       0: ldc           #4                  // class com/mango/newdemo/SingleTon
       2: aload_0
       3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #4                  // class com/mango/newdemo/SingleTon
       9: areturn

  static {};
    Code:
       0: new           #4                  // class com/mango/newdemo/SingleTon
       3: dup
       4: ldc           #7                  // String INSTANCE
       6: iconst_0
       7: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #9                  // Field INSTANCE:Lcom/mango/newdemo/SingleTon;
      13: iconst_1
      14: anewarray     #4                  // class com/mango/newdemo/SingleTon
      17: dup
      18: iconst_0
      19: getstatic     #9                  // Field INSTANCE:Lcom/mango/newdemo/SingleTon;
      22: aastore
      23: putstatic     #1                  // Field $VALUES:[Lcom/mango/newdemo/SingleTon;
      26: return
}

可以看到SingleTon 继承枚举类(说明枚举类是不能再继承其它类了),且有final修饰(即不能被继承),内部定义了一个静态常量INSTANCE,还定义了一个静态代码块用来对SingleTon对象的实例化并赋值给INSTANCE

当一个Java类第一次被真正使用到的时候静态资源被初始化,而Java类的加载和初始化过程都是线程安全的,因为loadClass方法内部是同步代码块,这点可以翻阅博主前面关于ClassLoader的文章,所以创建一个enum类型是线程安全的

有一句话叫做使用枚举实现单例是最好的实现方式,为什么这么说呢?

第一点:枚举的写法是六种写法中最简单的一种,没有之一(复杂操作由编译器帮我们干了)

第二点:枚举类的创建是自带线程安全的,无须开发者保证

第三点:也是最麻烦的一点,就是序列化的问题;要知道其它实现方式一旦实现了Serializable接口后,就不再是单例了,因为每次调用 readObject()方法返回的都是一个新创建出来的对象(有一种解决办法就是使用readResolve()方法来避免此事发生);但是枚举类是自己处理序列化,无须开发者动手;为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定,见下文

Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream;the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method,passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream.The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored–all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.

大概意思就是:

即在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法

可以看下枚举类的valueOf方法

public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {  
       T result = enumType.enumConstantDirectory().get(name);  
       if (result != null)  
           return result;  
       if (name == null)  
           throw new NullPointerException("Name is null");  
       throw new IllegalArgumentException(  
           "No enum const " + enumType +"." + name);  
}

在反序列化的时候,会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常;从enumConstantDirectory()方法中知道最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性;所以枚举类的序列化是得到虚拟机的保证的


Android中的单例

Application

每个APP的Application类是程序启动的入口,APP在首次启动时系统会创建一个Application对象并且是唯一的,并且它的声明周期是最长的,相当于整个应用的声明周期,这从侧面说明Application是一个单例的存在(仅限于单进程的应用,多进程下每个进程都会创建一个Application对象)

这样我们就可以在这个类里保存一些全局数据,进行一些资源的初始化操作,但是切记不要全部放在这个类里,这样会增加APP的启动时间,所以仅限于初始化部分重要的资源

单例引起的内存泄漏

因为单例对象的存在时间很长,相当于应用整个生命周期,如果构造单例对象的时候需要传入Context对象,且Context对象是从Activity或者Fragment获取的,那么当这些组件需要销毁时,GC在尝试回收该Activity或者Fragment时,发现它们还存在引用,即单例对象持有它们的引用,那这些组件就很难被回收,也就会发生内存泄漏;因此在需要持有Context的单例对象中该为持有Application的引用,比如传入context.getApplicaionContext

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值