你真的了解单例吗?

你真的了解单例吗?_第1张图片

最新在阅读《Android源码设计模式解析与实战》一书,我觉得写的很清晰,每一个知识点都有示例,通过示例更加容易理解。书中的知识点有些都接触过,有的没有接触过,总之,通过阅读这本书来梳理一下知识点,可能有些东西在项目中一直在使用,然并不能笼统,清理的说明理解它。本文主要是记录阅读这本书的知识点和自己的一些理解。一来整理知识点,二来方便以后查看,快速定位。

目录

1.定义
2.使用场景
3.实现方法
4.Android源码中的单例分析
5.总结

定义

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

使用场景

确保某个类只有一个实例,是避免多个对象耗费过多的资源。比如对IO活或数据库的操作。再比如ImageLoader(图片加载框架),它中有线程池,缓存系统,网路请求等都是非常消耗资源的。这时候应该考虑使用单例。

UML

你真的了解单例吗?_第2张图片
UML类图

角色介绍:

  • Client:高层客户端(调用端)
  • Singleton:单例类

实现单例类主要有以下几个关键点:

  • 1.构造函数不对外开放,一般为private
  • 2.通过一个静态方法或者枚举返回该类的实例
  • 3.确保该类只有一个实例,尤其是在多线程环境下
  • 4.确保单例对象在反序列化时不会重新构建对象

通过单例类构造方法的私有化,客户端不能通过new的方式创建对象,单例类会暴露一个公有的静态方法来获取该类的实例。在获取这个实例的过程中需要保证线程安全。这也是单例实现中比较困难的地方。

实现方式

1.饿汉式

public class Singleton {
    
    //在声明的时候就创建实例
    private static Singleton sInstance = new Singleton();
    
    //构造方法私有化
    private Singleton(){
        
    }
    
    //提供一个公有的静态方法来获取实例
    public static Singleton getInstance(){
        return sInstance;
    }
}

饿汉式: 在类创建的同时就已经创建好一个静态的对象供系统使用,以后不在改变。所以饿汉式是线程安全的。但是一开始就创建了实例,不管使不使用。

2.懒汉式

public class Singleton {
    
    private static Singleton sInstance;

    //构造方法私有化
    private Singleton(){}

    //提供一个公有的静态方法来获取实例
    public static synchronized Singleton getInstance(){
        if(sInstance == null){
            sInstance = new Singleton();
        }
        return sInstance;
    }
}

懒汉式:声明一个静态对象,并在第一次调用getInstance时进行初始化。但是这样是不能保证线程安全的。所以加上了synchronized,该方法就成了同步方法。但是这样的写法有一个问题就是每次调用getInstance都会进行同步,即使mInstance被初始化,这样会让费不必要的资源。

双重检查写法
public class Singleton {

    private static Singleton sInstance;

    //构造方法私有化
    private Singleton(){}

    //提供一个公有的静态方法来获取实例
    public static Singleton getInstance(){
        if(sInstance == null){
            synchronized(Singleton.class){
                if(sInstance == null){
                    sInstance = new Singleton();
                }
            }
        }
        return sInstance;
    }
}

getInstance方法中对mInstance进行了两次判空:第一次判空主要是避免不必要的同步;第二次判空是为了在null的情况下创建实例。

这是什么意思呢?假设A线程执行到了 sInstance = new Singleton();语句,看起来是一句代码但实际上不是一个原子操作,会被编译成多条汇编指令,它大概做了3件事:

  • (1)给Singleton实例分配内存
  • (2)调用Singleton的构造方法,初始化成员字段
  • (3)将sInstance指向分配的内存空间(此时sInstance不再是null了
    处理器为了提高程序运行效率,可能会对输入代码进行优化(指令重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说上面的第二和第三顺序是无法保证的,执行的顺序可能是1-2-3,也可能是1-3-2,如果是后者,并且在3执行完,2未执行的情况下切换到了B线程,因为在A中已经被实例化了,所以B直接使用,因为2还没有执行,所以就会报错。这就是这种写法的问题,并且这种问题很难跟踪。

Sung官方已经注意到这个问题了,所以在JDK1.5之后,调整了JVM,具体化了Volatile关键字,被它修饰的变量,每次读取都是从主内存读取的。所以在1.5版本之后,只需要改成private volatile static Singleton sInstance;就能保证线程安全了。虽然volatile会影响性能,但为了保证正确性,还是必要的。

双检查写法的优点:资源利用率高,只有在第一次调用的时候才会被初始化。缺点:第一次反应比较慢,高并发环境下也有一定的缺陷,虽然发生的概率比较小。这种写法是使用最多的单例实现方式,它能够在需要的时候创建实例,并且能够在绝大数情况下保证实例的唯一性,除非你的代码在并发环境非常复杂或者JDK版本1.5以下使用。否则,这种方式一般能够满足需求。

3.静态内部类单例

双检查的写法虽然子一定程度上解决了资源消耗,多余同步,线程安全的问题,但还是会在某些情况下失效。在《Java并发编程》一书中不赞成这种写法,而是用下面的代码替换:

public class Singleton {

    //构造方法私有化
    private Singleton(){}

    //提供一个公有的静态方法来获取实例
    public static Singleton getInstance(){
        return SingletonHolder.singleton;
    }

    //静态内部类
    private static class SingletonHolder{
        public static final Singleton singleton = new Singleton();
    }
}

当第一次加载Singleton类时并不会初始化sInstance,只有在第一次调用SingletongetInstance方法时sInstace才会被初始化。因此,第一次调用getInstace方法导致虚拟机加载SingletonHolder类,这种方式不仅能保证线程安全,还能保证单例对象的唯一性。所以这是推荐的方式。

4.枚举实现单例

public enum  SingletonEnum {
    
    INSTANCE;

    public void doSomething(){
        ........
    }
}

写法简单是枚举单例的最大优点,枚举和类一样,不仅能够拥有字段还能有自己的方法。最重要的是默认枚举实例的创建是线程安全的,任何情况下都是一个实例。

为什么说枚举单例在任何情况下都是一个实例呢?在上述的几种单例实现中,在一种情况下它们都会重新创建实例,那就是反序列化。

通过序列化可以将一个对象写到磁盘,然后再读回来,从而获得一个实例。即使构造方法是私有的,在反序列化的时候也可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造方法。

反序列化提供了一个很特别分钩子函数,类中有一个私有的,被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。上述单例的实现要想在反序列化中杜绝生成新的实例,就要加入如下的方法:

 private Object readResolve() throws ObjectStreamException{
        return sInstance;
 }

也就是在readResolve方法中返回类的实例,而不是默认的重新生成一个对象。而对于枚举并不存在这个问题。

5.使用容器实现单例

public class SingletonManager {

    private static Map objMap = new HashMap<>();

    private SingletonManager(){}

    public static void registerService(String key,Object value){
        if(!objMap.containsKey(key)){
            objMap.put(key,value);
        }
    }

    public static Object getService(String key){
        return objMap.get(key);
    }
}

在程序的初始,将多种单例统一注入到一个管理类中,在使用时根据key来获取对应的对象。这种方式可以统一管理多种类型的单例,并且在使用时可以统一的接口进行操作,降低了用户的使用成本,对用户隐藏了具体的实现细节,降低了耦合。

Android源码中的单例

在Android系统中,我们经常通过Context获取系统级别服务,比如:ActivityManager,WindowManager等,更常用的是一个LayoutInflater类,这些服务都会在合适的时候以单例的方式注册在系统中,在我们需要的时候就通过Context的getSystemService(String name)来获取,我们以Layoutflater为例来说明,平时Layoutflater比较常见的地方是列表的适配器中使用:

 @Override
 public View getView(int position, View contentView, ViewGroup viewGroup) {
     View itemView = null;
     if(contentView == null){
         itemView = LayoutInflater.from(mContext).inflate(mLayoutId,null);
     }else{
         //代码省略
     }
     //代码省略
     return itemView;
 }

通过我们使用LayoutInflater.from(Context context)来获取LayoutInflater服务,下面我们看一下它的实现:

你真的了解单例吗?_第3张图片

可以看到from(Context context)方法内部是调用的是Context类的getSystemService(String key)方法,我们看一下Context

你真的了解单例吗?_第4张图片

Context是一个抽象类,那么getViewContext对象的具体实现类是什么呢?通过列表都是在Activity中显示的,我们传入一般都是Activity中的Context,通过查看源码一路追踪我们发现ActivityContext的具体实现类是ContextImpl

你真的了解单例吗?_第5张图片

ContextImpl部分代码可以看到,在虚拟机第一次加载该类的时候会注册各种StaticServiceFetcher,其中就包含了LayoutInflater Service,将这些服务以键值对的形式存储在一个HashMap中,用户在使用时只需要根据Key来获取对应的ServiceFetcher,然后通过对应的ServiceFetcher对象的getService方法来获取具体的服务对象。当第一次调用的时候会调用ServiceFetchercreateService方法来创建服务对象,然后将该对象存储到一个列表中,下次再使用时直接从列表中取,从而达到单例的效果。

总结

不管哪种方式的单例,它们的核心原理都是将构造方法私有化,并且通过一个静态方法来获取一个唯一的实例,在这个获取的过程中必须保证线程安全,防止反序列化导致重新生成对象等问题。具体选择哪一种,要根据项目本身,如是否复杂的并发环境,JDK版本是否过低,单例对象的资源消耗等。

你可能感兴趣的:(你真的了解单例吗?)