介绍
单例对象的类必须保证只有一个实例存在。
某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
场景
避免产生多个对象消耗过多资源,或者某种类型的对象只应该有且只有一个。例如创建一个对象需要访问IO和数据库等资源,这时候就该考虑使用单例模式。
关键点
- 构造函数不对外开放,一般为private
- 通过一个静态方法或者枚举返回实例。
- 确保实例只有一个,尤其是在多线程的情况下。
- 确保单例对象在反序列化时不会重新构件对象。
简单示例
-
懒汉模式
懒汉模式声明一个静态对象,当用户第一次调用getInstance时进行初始化。具体实现如下:/** * 懒汉模式 * Created by Bowen on 2016-04-20. */ public class Singleton1 { private static Singleton1 mSingleton; private Singleton1(){ } private synchronized static Singleton1 getInstance(){ if(mSingleton == null){ mSingleton = new Singleton1(); } return mSingleton; } }
需要注意的是,在getInstance方法上,我们使用了synchronized关键字修饰,这就是为了考虑到 关键点 中的第3点(确保实例只有一个,尤其是在多线程的情况下),考虑到如果有两个线程同时调用了getInstance方法,两个线程在进行mSingleton == null的判断时,结果都是true,导致mSingleton被实例化了两次,为了避免这种情况,我们需要用synchronized进行同步,但这又会导致另一个效率问题,每次调用getInstance方法时,都会进行同步,会造成不必要的同步开销。另外,懒汉模式在第一次调用getInstance时才进行实例化,反应稍慢。
-
饿汉模式
饿汉模式是在声明对象时就已经完成了初始化,具体实现如下:/** * 饿汉模式 * Created by Bowen on 2016-04-21. */ public class Singleton2 { private static Singleton2 mSingleton = new Singleton2(); private Singleton2(){ } public static Singleton2 getInstance(){ return mSingleton; } }
在饿汉模式中,并没有出现懒汉模式的问题:每次调用getInstance都需要同步。
-
Double CheckLock(DCL)模式
DCL模式实现了需要时实例化,并保证了线程安全,同时单例对象初始化后以后不进行同步锁:/** * Double CheckLock(DCL)模式 * * Created by Bowen on 2016-04-21. */ public class Singleton3 { private volatile static Singleton3 mSingleton; private Singleton3(){ } public static Singleton3 getInstance(){ if (mSingleton == null){ synchronized (Singleton3.class){ if (mSingleton == null){ mSingleton = new Singleton3(); } } } return mSingleton; } }
为了避免单例对象实例化以后进行不必要的同步,所以对实例对象进行第一次判空,如果此时实例对象已经被实例化,那么将不会执行同步代码块。当同步后依然为空,另外,用volatile关键字修饰实例对象,保证每一次都从主存中读取对象,避免多线程的情况下出现问题。
-
静态内部类单例模式
/** * 静态内部类单例模式 * * Created by Bowen on 2016-04-21. */ public class Singleton4 { private Singleton4(){ } public static Singleton4 getInstance(){ return SingletonHolder.mInstance; } private static class SingletonHolder{ private static final Singleton4 mInstance = new Singleton4(); } }
当用户第一次调用getInstance方法时,mInstance才被初始化。静态内部类的单例模式,不仅保证了线程安全,也保证了对象的唯一性,并延迟了对象实例化的时间。
-
枚举单例
/** * 枚举单例 * * Created by Bowen on 2016-04-21. */ public enum Singleton5 { INSTANCE; public void doWork(){ } }
默认枚举示例的创建时线程安全的,并且反序列化的情况下仍然是一个单例。
Android源码中的单例模式
LayoutInflater类,我们经常会在ListView中的getView方法使用到LayoutInflater,示例代码:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null){
convertView = LayoutInflater.from(mContext).inflate(layoutiId,parent,false);
}else {
}
return convertView;
}
通过LayoutInflater.from(context)来获取LayoutInflater的服务,下面是具体实现:
/**
* Obtains the LayoutInflater from the given context.
*/
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
因为Context是一个Abstract类,getSystemService方法的具体实现在Context的实现类中,进入ContextImpl类定位到具体实现:
public Object Object getService(ContextImpl ctx) {
ArrayList<Object> cache = ctx.mServiceCache;
Object service;
synchronized (cache) {
if (cache.size() == 0) {
// Initialize the cache vector on first access.
// At this point sNextPerContextServiceCacheIndex
// is the number of potential services that are
// cached per-Context.
for (int i = 0; i < sNextPerContextServiceCacheIndex; i++) {
cache.add(null);
}
} else {
service = cache.get(mContextCacheIndex);
if (service != null) {
return service;
}
}
service = createService(ctx);
cache.set(mContextCacheIndex, service);
return service;
}
}
可以看到。ContextImpl中使用了一个HashMap存放何种Service,用户根据serviceName作为key查找对应的的服务,当第一次获取时,创建对象后并将对象存入map中,下次使用时只要从map中取出即可。
总结
- 优点
- 单例模式在内中只有一个示例,减少内存开支。
- 减少系统的性能开销、当一个对象的产生需要较多资源时,如读取配置产生其他以来对象时,则可以通过应用启动时直接产生一个单例对象,然后用永久驻留内存的方式解决。
- 避免对资源的多重占用,例如对文件操作,由于只有一个示例存在内存中,避免对同一个资源文件同时写操作。
- 在系统设置全局的访问点,优化和共享资源访问。
- 缺点
- 单例模式一般没有接口,扩展比较困难,如果要扩展,必须要修改代码。
- 如果单例对象持有Context,则很容易引起内存泄露(单例模式的生命周期和应用一样),所以最好传递Application的Context。