单例模式总结

定义

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

使用场景

确保某个类有且只有一个的场景,避免消耗过多资源,或者某种类型的对象只应该有且只有一个。

如:访问IO,数据库,打印机等等

关键点

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

类型

1. 懒汉模式(一般不建议使用)
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}

  • 优点:只有在第一次调用才初始化,在一定程度上节约了资源。
  • 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
2. 饿汉模式
public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
}

  • 优点:没有加锁,执行效率会提高。
  • 缺点:类加载时就初始化,浪费内存。

基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

3. 双检锁/双重校验锁(DCL,即 double-checked locking)
public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}

  • 优点:资源利用率高
  • 缺点:不适合高并发或者JDK6以下使用

这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

注意点:需要在JDK1.5之后才能使用。

若singleton定义时不加volatile,有可能会造成失效。

原因
假设线程A执行到singleton = new Singleton()语句,这看起来是一句代码,但实际上着并非是一个原子操作,这句代码会被翻译成多条汇编指令,大致做了三件事:

  1. 给Singleton的实例分配内存
  2. 调用Singleton()的构造函数,初始化成员字段
  3. 将singleton字段指向分配的内存空间(此时singleton就不是null了)

但是由于JAVA编译器允许处理器乱序执行,以及JDK1.5之前JMM中Cache,寄存器到主内存回写顺序的规定,上面的第二和第三的顺序是无法保证的,执行顺序可能是1-2-3,也可能是1-3-2。如果是后者,并且在3执行完毕2执行之前,被切换到B线程上,此时singleton因为在A线程中已经执行过第三点,已经是非空了,所以B线程会直接取走singleton,此时使用就会出错,这就是DCL失效问题。

在JDK1.5之后SUN官方已经注意到这种问题了,调整了JVM,具体化了volatile关键字,因此如果JDK1.5之后,只要加上volatile关键字,就可以保证singleton对象每次都是从主内存读取,此时就可以正常使用DCL单例模式。

DCL虽然在一定程度上解决了资源消耗,多余的同步,线程安全等问题,但是,他还是在某些情况下出现失效的问题,在《JAVA并发编程实践》一书最后谈到了这个问题,建议使用静态内部类单例模式代替。

4. 静态内部类单例模式/登记式(推荐)
public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}

这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。

类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

  • 优点:延迟了单例的实例化。
  • 缺点:反序列化不特殊处理会重新生成对象

上述方式中如何杜绝反序列化时重新生成对象:
加入readResolve函数,在readResolve方法中将单例对象返回,而不是重新创建新的对象。

  • 可序列化类中的字段类型不是Java内置类型,那么该字段也需要实现Serializable接口。
  • 如果你调整了可序列化类的内部结构,例如新增去除某个字段,但没有修改serialVersionUID,那么会引发java.io.IvalidClassException异常或者导致某个属性为0或者null。此时我们可以直接将serialVersionUID设置为0L,这样即使修改了类的内部结构,我们反序列化也不会抛
    java.io.IvalidClassException,只是那些新修改的字段会为0或者null.
public class Singleton implements Serializable { 
    private static final long serialVersionUID = 0L;
    
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    
    private Singleton (){}  
    
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    } 
    
    
    private Object readResolve() throws ObjectStreamException {
        return SingletonHolder.INSTANCE;
    }
}
5.枚举单例(推荐)
public enum Singleton {  
    INSTANCE;  
    public void doSomething() {  
    }  
}
  • 优点:写法简单,是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。防止反射强行调用构造器。
  • 缺点: JDK1.5 之后才加入 enum 特性。在Android中却不是特别推荐:Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
6.使用容器实现单例(管理多种类型的单例对象)
public class SingletonManager { 
  private static Map<String, Object> objMap = new HashMap<String,Object>();
  private Singleton() { 
  }
  public static void registerService(String key, Objectinstance) {
    if (!objMap.containsKey(key) ) {
      objMap.put(key, instance) ;
    }
  }
  public static ObjectgetService(String key) {
    return objMap.get(key) ;
  }
}

如何选择

  • 是否是复杂的并发环境
  • JDK版本是否过低
  • 单例对象的资源消耗,lazy loading
  • 等等

一般情况下,不建议使用懒汉方式,建议使用饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用登记方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用双检锁方式。

小结

客户端中一般没有高并发的情况,出于效率考虑一般推荐使用双检锁/双重校验锁(DCL)或者静态内部类单例模式/登记式。

单例模式的缺点:

  • 单例模式一般没有接口,拓展困难,只能修改代码
  • 单例对象如果持有Context,容易引发内存泄漏,此时需要注意传给单例对象的Context最好是Application Context

Android源码中的单例模式(拓展)

如何获取系统服务(ServiceFetcher)

在6.0之前是直接写在ContextImpl.java中(可参考Android源码设计模式解析与实战第二章的讲解),之后写在SystemServiceRegistry.java中,这里采用最新的Android8.0代码

final class SystemServiceRegistry {

// Service registry information.
    // This information is never changed once static initialization has completed.
    private static final HashMap<Class<?>, String> SYSTEM_SERVICE_NAMES =
            new HashMap<Class<?>, String>();
    private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
            new HashMap<String, ServiceFetcher<?>>();
    private static int sServiceCacheSize;
    
    static {
        registerService(Context.ACCESSIBILITY_SERVICE, AccessibilityManager.class,
                new CachedServiceFetcher<AccessibilityManager>() {
            @Override
            public AccessibilityManager createService(ContextImpl ctx) {
                return AccessibilityManager.getInstance(ctx);
            }});
            
            //同样方式注册各种服务
            ....
        }
        
    /**
     * Gets a system service from a given context.
     */
    public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }
    
    /**
     * Statically registers a system service with the context.
     * This method must be called during static initialization only.
     */
    private static <T> void registerService(String serviceName, Class<T> serviceClass,
            ServiceFetcher<T> serviceFetcher) {
        SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
        SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
    }

    /**
     * Base interface for classes that fetch services.
     * These objects must only be created during static initialization.
     */
    static abstract interface ServiceFetcher<T> {
        T getService(ContextImpl ctx);
    }
    
        /**
     * Override this class when the system service constructor needs a
     * ContextImpl and should be cached and retained by that context.
     */
    static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> {
        private final int mCacheIndex;

        public CachedServiceFetcher() {
            mCacheIndex = sServiceCacheSize++;
        }

        @Override
        @SuppressWarnings("unchecked")
        public final T getService(ContextImpl ctx) {
            final Object[] cache = ctx.mServiceCache;
            synchronized (cache) {
                // Fetch or create the service.
                Object service = cache[mCacheIndex];
                if (service == null) {
                    try {
                        service = createService(ctx);
                        cache[mCacheIndex] = service;
                    } catch (ServiceNotFoundException e) {
                        onServiceNotFound(e);
                    }
                }
                return (T)service;
            }
        }

        public abstract T createService(ContextImpl ctx) throws ServiceNotFoundException;
    }

    /**
     * Like StaticServiceFetcher, creates only one instance of the service per application, but when
     * creating the service for the first time, passes it the application context of the creating
     * application.
     *
     * TODO: Delete this once its only user (ConnectivityManager) is known to work well in the
     * case where multiple application components each have their own ConnectivityManager object.
     */
    static abstract class StaticApplicationContextServiceFetcher<T> implements ServiceFetcher<T> {
        private T mCachedInstance;

        @Override
        public final T getService(ContextImpl ctx) {
            synchronized (StaticApplicationContextServiceFetcher.this) {
                if (mCachedInstance == null) {
                    Context appContext = ctx.getApplicationContext();
                    // If the application context is null, we're either in the system process or
                    // it's the application context very early in app initialization. In both these
                    // cases, the passed-in ContextImpl will not be freed, so it's safe to pass it
                    // to the service. http://b/27532714 .
                    try {
                        mCachedInstance = createService(appContext != null ? appContext : ctx);
                    } catch (ServiceNotFoundException e) {
                        onServiceNotFound(e);
                    }
                }
                return mCachedInstance;
            }
        }

        public abstract T createService(Context applicationContext) throws ServiceNotFoundException;
    }
}

在虚拟机第一次加载该类的时候会注册各种ServiceFetcher,将这些服务以键值对的形式存储在一个HashMap中,用户只需要根据Key来获取对应的ServiceFetcher,然后通过ServiceFetcher对象的getService(ContextImpl ctx)方法来获取具体的服务对象。在第一次获取时,会调用ServiceFetcher的createService(ContextImpl ctx)函数创建服务对象,然后缓存到一个cache数组中,下次再取时直接从cache中获取,避免重复创建对象,达到单例的效果。这种方式就是通过容器的单例模式实现方式,系统服务以单例的形式存在,减少资源消耗。

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

推荐阅读更多精彩内容

  • 单例模式(SingletonPattern)一般被认为是最简单、最易理解的设计模式,也因为它的简洁易懂,是项目中最...
    成热了阅读 4,246评论 4 34
  • 使用场景 要求生成唯一序列号的环境 在整个项目中需要一个共享访问点或共享数据例如一个Web页面上的计数器,可以不用...
    niaoge2016阅读 422评论 0 0
  • 前言 本文主要参考 那些年,我们一起写过的“单例模式”。 何为单例模式? 顾名思义,单例模式就是保证一个类仅有一个...
    tandeneck阅读 2,507评论 1 8
  • 单例模式 定义:确保某一个类只有一个实例,自行实例化并且想整个系统提供这个实例。 使用场景:避免某个类产生多个对象...
    luoyoub阅读 193评论 0 0
  • 简书,我起初是被这个软件的名字所吸引。 何谓简?简,竹子搭建的一座房屋。 简洁大方的外观,清雅别致的界面,以及美好...
    悒郁的肖像阅读 900评论 14 23