单例模式总结

定义

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

使用场景

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

如:访问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中获取,避免重复创建对象,达到单例的效果。这种方式就是通过容器的单例模式实现方式,系统服务以单例的形式存在,减少资源消耗。

    原文作者:烟火雨落
    原文地址: https://www.jianshu.com/p/4426ad5cdfe7
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞