你真的了解单例吗?
最新在阅读《Android源码设计模式解析与实战》一书,我觉得写的很清晰,每一个知识点都有示例,通过示例更加容易理解。书中的知识点有些都接触过,有的没有接触过,总之,通过阅读这本书来梳理一下知识点,可能有些东西在项目中一直在使用,然并不能笼统,清理的说明理解它。本文主要是记录阅读这本书的知识点和自己的一些理解。一来整理知识点,二来方便以后查看,快速定位。
目录
1.定义
2.使用场景
3.实现方法
4.Android源码中的单例分析
5.总结
定义
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
使用场景
确保某个类只有一个实例,是避免多个对象耗费过多的资源。比如对IO活或数据库的操作。再比如ImageLoader(图片加载框架),它中有线程池,缓存系统,网路请求等都是非常消耗资源的。这时候应该考虑使用单例。
UML
角色介绍:
* Client:高层客户端(调用端)
* Singleton:单例类实现单例类主要有以下几个关键点:
* 1.构造函数不对外开放,一般为private
* 2.通过一个静态方法或者枚举返回该类的实例
* 3.确保该类只有一个实例,尤其是在多线程环境下
* 4.确保单例对象在反序列化时不会重新构建对象
通过单例类构造方法的私有化,客户端不能通过new的方式创建对象,单例类会暴露一个公有的静态方法来获取该类的实例。在获取这个实例的过程中需要保证线程安全。这也是单例实现中比较困难的地方。
实现方式
1.饿汉式
public class Singleton {
//在声明的时候就创建实例
private static Singleton sInstance = new Singleton();
//构造方法私有化
private Singleton(){
}
//提供一个公有的静态方法来获取实例
public static Singleton getInstance(){
return sInstance;
}
}
饿汉式: 在类创建的同时就已经创建好一个静态的对象供系统使用,以后不在改变。所以饿汉式是线程安全的。但是一开始就创建了实例,不管使不使用。
2.懒汉式
public class Singleton {
private static Singleton sInstance;
//构造方法私有化
private Singleton(){}
//提供一个公有的静态方法来获取实例
public static synchronized Singleton getInstance(){
if(sInstance == null){
sInstance = new Singleton();
}
return sInstance;
}
}
懒汉式:声明一个静态对象,并在第一次调用
getInstance
时进行初始化。但是这样是不能保证线程安全的。所以加上了synchronized
,该方法就成了同步方法。但是这样的写法有一个问题就是每次调用getInstance
都会进行同步,即使mInstance
被初始化,这样会让费不必要的资源。
双重检查写法
public class Singleton {
private static Singleton sInstance;
//构造方法私有化
private Singleton(){}
//提供一个公有的静态方法来获取实例
public static Singleton getInstance(){
if(sInstance == null){
synchronized(Singleton.class){
if(sInstance == null){
sInstance = new Singleton();
}
}
}
return sInstance;
}
}
在
getInstance
方法中对mInstance进行了两次判空:第一次判空主要是避免不必要的同步;第二次判空是为了在null
的情况下创建实例。这是什么意思呢?假设A线程执行到了
sInstance = new Singleton();
语句,看起来是一句代码但实际上不是一个原子操作,会被编译成多条汇编指令,它大概做了3件事:
* (1)给Singleton
实例分配内存
* (2)调用Singleton
的构造方法,初始化成员字段
* (3)将sInstance
指向分配的内存空间(此时sInstance
不再是null了
)
处理器为了提高程序运行效率,可能会对输入代码进行优化(指令重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说上面的第二和第三顺序是无法保证的,执行的顺序可能是1-2-3,也可能是1-3-2,如果是后者,并且在3执行完,2未执行的情况下切换到了B线程,因为在A中已经被实例化了,所以B直接使用,因为2还没有执行,所以就会报错。这就是这种写法的问题,并且这种问题很难跟踪。Sung官方已经注意到这个问题了,所以在JDK1.5之后,调整了JVM,具体化了
Volatile
关键字,被它修饰的变量,每次读取都是从主内存读取的。所以在1.5版本之后,只需要改成private volatile static Singleton sInstance;
就能保证线程安全了。虽然volatile
会影响性能,但为了保证正确性,还是必要的。
双检查写法的优点:资源利用率高,只有在第一次调用的时候才会被初始化。缺点:第一次反应比较慢,高并发环境下也有一定的缺陷,虽然发生的概率比较小。这种写法是使用最多的单例实现方式,它能够在需要的时候创建实例,并且能够在绝大数情况下保证实例的唯一性,除非你的代码在并发环境非常复杂或者JDK版本1.5以下使用。否则,这种方式一般能够满足需求。
3.静态内部类单例
双检查的写法虽然子一定程度上解决了资源消耗,多余同步,线程安全的问题,但还是会在某些情况下失效。在《Java并发编程》一书中不赞成这种写法,而是用下面的代码替换:
public class Singleton {
//构造方法私有化
private Singleton(){}
//提供一个公有的静态方法来获取实例
public static Singleton getInstance(){
return SingletonHolder.singleton;
}
//静态内部类
private static class SingletonHolder{
public static final Singleton singleton = new Singleton();
}
}
当第一次加载
Singleton
类时并不会初始化sInstance
,只有在第一次调用Singleton
的getInstance
方法时sInstace
才会被初始化。因此,第一次调用getInstace
方法导致虚拟机加载SingletonHolder
类,这种方式不仅能保证线程安全,还能保证单例对象的唯一性。所以这是推荐的方式。
4.枚举实现单例
public enum SingletonEnum {
INSTANCE;
public void doSomething(){
........
}
}
写法简单是枚举单例的最大优点,枚举和类一样,不仅能够拥有字段还能有自己的方法。最重要的是默认枚举实例的创建是线程安全的,任何情况下都是一个实例。
为什么说枚举单例在任何情况下都是一个实例呢?在上述的几种单例实现中,在一种情况下它们都会重新创建实例,那就是反序列化。
通过序列化可以将一个对象写到磁盘,然后再读回来,从而获得一个实例。即使构造方法是私有的,在反序列化的时候也可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造方法。
反序列化提供了一个很特别分钩子函数,类中有一个私有的,被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。上述单例的实现要想在反序列化中杜绝生成新的实例,就要加入如下的方法:
private Object readResolve() throws ObjectStreamException{
return sInstance;
}
也就是在readResolve方法中返回类的实例,而不是默认的重新生成一个对象。而对于枚举并不存在这个问题。
5.使用容器实现单例
public class SingletonManager {
private static Map<String,Object> objMap = new HashMap<>();
private SingletonManager(){}
public static void registerService(String key,Object value){
if(!objMap.containsKey(key)){
objMap.put(key,value);
}
}
public static Object getService(String key){
return objMap.get(key);
}
}
在程序的初始,将多种单例统一注入到一个管理类中,在使用时根据key来获取对应的对象。这种方式可以统一管理多种类型的单例,并且在使用时可以统一的接口进行操作,降低了用户的使用成本,对用户隐藏了具体的实现细节,降低了耦合。
Android源码中的单例
在Android系统中,我们经常通过Context获取系统级别服务,比如:ActivityManager,WindowManager等,更常用的是一个LayoutInflater类,这些服务都会在合适的时候以单例的方式注册在系统中,在我们需要的时候就通过Context的getSystemService(String name)来获取,我们以Layoutflater为例来说明,平时Layoutflater比较常见的地方是列表的适配器中使用:
@Override
public View getView(int position, View contentView, ViewGroup viewGroup) {
View itemView = null;
if(contentView == null){
itemView = LayoutInflater.from(mContext).inflate(mLayoutId,null);
}else{
//代码省略
}
//代码省略
return itemView;
}
通过我们使用LayoutInflater.from(Context context)
来获取LayoutInflater
服务,下面我们看一下它的实现:
可以看到from(Context context)
方法内部是调用的是Context类的getSystemService(String key)
方法,我们看一下Context
类
Context
是一个抽象类,那么getView
中Context
对象的具体实现类是什么呢?通过列表都是在Activity
中显示的,我们传入一般都是Activity中的Context
,通过查看源码一路追踪我们发现Activity
中Context
的具体实现类是ContextImpl
从
ContextImpl
部分代码可以看到,在虚拟机第一次加载该类的时候会注册各种StaticServiceFetcher
,其中就包含了LayoutInflater Service
,将这些服务以键值对的形式存储在一个HashMap
中,用户在使用时只需要根据Key来获取对应的ServiceFetcher
,然后通过对应的ServiceFetcher
对象的getService
方法来获取具体的服务对象。当第一次调用的时候会调用ServiceFetcher
的createService
方法来创建服务对象,然后将该对象存储到一个列表中,下次再使用时直接从列表中取,从而达到单例的效果。
总结
不管哪种方式的单例,它们的核心原理都是将构造方法私有化,并且通过一个静态方法来获取一个唯一的实例,在这个获取的过程中必须保证线程安全,防止反序列化导致重新生成对象等问题。具体选择哪一种,要根据项目本身,如是否复杂的并发环境,JDK版本是否过低,单例对象的资源消耗等。