你真的了解单例吗?

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

目录

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

定义

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

使用场景

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

UML

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版本是否过低,单例对象的资源消耗等。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容