定义
确保一个类只有一个实例,并且自行实例化并向整个系统提供这个实例。
使用场景
确保某个类有且只有一个的场景,避免消耗过多资源,或者某种类型的对象只应该有且只有一个。
如:访问IO,数据库,打印机等等
关键点
- 构造函数不对外开放,一般为private
- 通过一个静态方法或者枚举返回单例对象
- 确保单例类的对象有且只有一个,尤其在多线程环境下
- 确保单例类对象在反序列化时不会重新创建对象
类型
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()语句,这看起来是一句代码,但实际上着并非是一个原子操作,这句代码会被翻译成多条汇编指令,大致做了三件事:
- 给Singleton的实例分配内存
- 调用Singleton()的构造函数,初始化成员字段
- 将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 objMap = new HashMap();
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, String> SYSTEM_SERVICE_NAMES =
new HashMap, String>();
private static final HashMap> SYSTEM_SERVICE_FETCHERS =
new HashMap>();
private static int sServiceCacheSize;
static {
registerService(Context.ACCESSIBILITY_SERVICE, AccessibilityManager.class,
new CachedServiceFetcher() {
@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 void registerService(String serviceName, Class serviceClass,
ServiceFetcher 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 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 implements ServiceFetcher {
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 implements ServiceFetcher {
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中获取,避免重复创建对象,达到单例的效果。这种方式就是通过容器的单例模式实现方式,系统服务以单例的形式存在,减少资源消耗。