欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

你真的了解单例吗?

程序员文章站 2022-04-09 23:19:14
...

你真的了解单例吗?

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

目录

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

定义

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

使用场景

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

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<String,Object> 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服务,下面我们看一下它的实现:
你真的了解单例吗?

可以看到from(Context context)方法内部是调用的是Context类的getSystemService(String key)方法,我们看一下Context
你真的了解单例吗?

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

你真的了解单例吗?

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

总结

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

关注微信公众号获取更多相关资源

你真的了解单例吗?