定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决什么
一个全局使用的类频繁地创建与销毁。
何时使用
当您想控制实例数目,节省系统资源的时候。
如何解决
判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码
构造函数是私有的。
单例的写法——七种
饿汉式、懒汉式(线程不安全)、懒汉式(线程安全)、双重校验锁、登记式/静态内部类、枚举、使用容器实现。
饿汉式
类加载时就初始化。非懒加载;是多线程安全。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
懒汉式(线程不安全)
第一次调用才初始化。懒加载;非多线程安全。
这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式(线程安全)
第一次调用才初始化。懒加载;是多线程安全。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
每次调用的时候 getInstance 都进行同步,造成不必要的开销。这种模式一般不建议使用。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
DLC双重校验锁
采用双锁机制,安全且在多线程情况下能保持高性能。懒加载;是多线程安全。
既能够在需要时初始化单例,又能保证线程安全,且单例对象初始化后调用 instance 不进行同步锁。
优点:资源利用率高,第一次执行 getInstance 时单例对象才会被实例化,效率高。
缺点:第一次加载时,反应稍慢,也由于 Java 内存模型的原因偶尔会失败。在高并发环境下也有一定的缺陷,虽然发生概率很小。
DCL 模式是使用最多的模式,它能够在需要时才被实例化,并且能够在绝大多数场景下保证单例对象的唯一性,除非你的代码在并发场景比较复杂或者低于 JDK 6 版本下使用,否则,这种方式一般能够满足需求。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) { // 第一层判断主要是为了避免不必要的同步
synchronized (Singleton.class) {
if (singleton == null) { // 第二层的判断则是为了在 null 的情况下创建实例
singleton = new Singleton();
}
}
}
return singleton;
}
}
登记式/静态内部类
懒加载;是多线程安全。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟饿汉式不同的是:饿汉式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。
静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如 Context 这种参数,所以,我们创建单例时,可以在静态内部类与 DCL 模式里自己斟酌。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
枚举
非懒加载;是多线程安全。
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
优点:枚举本身是线程安全的,且能防止通过反射和反序列化创建实例。
缺点:对 JDK 版本有限制要求(JDK1.5 之后才加入 enum 特性),非懒加载。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
使用容器实现
用SingletonManager 将多种的单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。
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) ;
}
}
android中的应用
容器单例模式,以 Context.LAYOUT_INFLATER_SERVICE 举例。
从 setContentView 入口,全方位分析 LayoutInflater
Android中的系统级服务都是通过容器的单例模式实现方式,以单例形式存在,减少了资源消耗。
比如LayoutInflater Service,将这些服务以键值对的形式存储在一个HashMap容器中,用户使用时只需要根据key来获取到对应的ServiceFetcher,然后通过ServcieFetcher对象的getService函数来获取到具体的服务对象,第一次获取时会调用ServcieFetcher的createService函数创建服务对象,然后将该对象缓存到一个列表中,下次再取时直接从缓存中获取,避免重复创建对象,从而达到单例的效果。
总结
一般情况下,不建议使用懒汉方式,建议使用饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用登记方式(静态内部类)。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用双检锁方式。
参考资料
设计模式 (一) 通过理论 + 代码示例 + Android 源码中单例模式来学习单例