单例模式是应用最广的模式。其实经常使用的图片加载框架ImageLoader的实例创建就是使用了单例模式,因为这个ImageLoader中含有线程池、缓存系统、网络请求,很消耗资源,不应该创建多个对象,这时候就需要用到单例模式。
ImageLoader的创建代码如下:
ImageLoader.getInstance();// 在自己的Application中创建全局实例
.....
//getInstance()执行的源码
public static ImageLoader getInstance() {
if(instance == null) {//双重校验DCL单例模式
Class var0 = ImageLoader.class;
synchronized(ImageLoader.class) {//同步代码块
if(instance == null) {
instance = new ImageLoader();//创建一个新的实例
}
}
}
return instance;//返回一个实例
}
因此,在我们创建一个对象需要消耗过多的资源时,便可以考虑使用单例模式。
单例模式的定义是它应该保证一个类仅有一个实例,同时这个类还必须提供一个访问该类的全局访问点。如下图是单例模式的结构图:
实现单例模式有以下几个关键点:
1.在调用getInstance()方法时返回一个且唯一的Singleton对象。
2.能够在多线程使用时也能保证获取的Singleton对象唯一
3.getInstance()方法的性能要保证
4.能在需要的时候才初始化,否则不用初始化
通过将单例类的构造函数私有化,使得客户端不能通过new的形式手动构造单例类的对象。单例类会主动暴露一个公有的静态方法,客户端调用这个静态的方法获取到单例类的唯一实例。在获取这个单例类的时候需要确保这个过程是线程安全的。
/**
* 饿汉式
* 基于ClassLoader的机制,在同一classLoader下,该方式可以解决多线程同步的问题,
* 但是该种单例模式没有办法实现懒加载
*/
public class Singleton {
//私有的构造函数
private Singleton() {
}
//私有的静态变量 static修饰的静态变量在内存中一旦创建,便永久存在
private static Singleton single = new Singleton();
//暴露的公有静态方法
public static Singleton getInstance() {
return single;
}
}
以上就是饿汉式的写法,满足了上边说的第1,2条要求。该模式有几点要注意:
1.默认构造方法需要私有化,不然外部可以随时的构造方法,这样就没法保证单例了。
2.Singleton 类型的静态变量mInstance也是私有化的。这样外部就不能直接获取到mInstance,并且正是由于mInstance是静态变量并且声明时就初始化了,我们知道根据java虚拟机和ClassLoader的特性,一个类在一个ClassLoader中只会被加载一次,天生就是线程安全的。并且这里的mInstance在加载时就已经初始化了,这可以确定对象的唯一性。也就是说保证了在多线程并发情况下获取到的对象是唯一的。
其中instance=new Singleton()可以写成:
static {
instance = new Singleton();
}
属于变种的饿汉单例模式,也是基于classloder机制避免了多线程的同步问题,instance在类装载时就实例化了。
其中起到重要作用的是静态修饰符static关键字,我们知道在程序中,任何变量或者代码都是在编译时由系统自动分配内存来存储的,而所谓静态就是指在编译后所分配的内存会一直存在(GC Root对象:方法区静态引用指向的对象),直到程序退出内存才会释放这个空间,因此也就保证了单例类的实例一旦创建,便不会被系统回收,除非手动设置为null。
该种方式肯定也是有明显的缺点,就是不能满足上边要求中的第3点,例如某类实例需求依赖在运行时的参数来生成,那么由于饿汉式在类加载时就已经初始化了,所以无法满足懒加载。此外,从始至终未使用过该实例,造成内存浪费。那我们就来看看懒加载的写法。
//懒汉式单例类.在第一次调用的时候实例化自己
public class Singleton {
//私有的构造函数
private Singleton() {
}
//私有的静态变量
private static Singleton single =null;
//暴露的公有静态方法
public static Singleton getInstance() {
if (single == null) { //line1
single=new Singleton(); //line2
}
return single;
}
}
可以看出确实是在调用getInstance()方法时,才会初始化实例,实现了懒加载。但是在能否满足在多线程下正常工作呢?我们在这里先分析一下假设有两个线程ThreadA和ThreadB:
ThreadA首先执行到line1,这时mInstance为null,ThreadA将接着执行new Singleton();在这个过程中如果mInstance已经分配了内存地址,但是还没有完成初始化工作(问题就出在这儿,稍后分析),如果ThreadB执行了line1,因为mInstance已经指向了某一内存,所以将跳过new Singleton()直接得到mInstance,但是此时mInstance还没有完成初始化,那么问题就出现了。造成这个问题的原因就是new Singleton()这个操作不是原子操作。至少可以分解成以下上个原子操作:
1.分配内存空间
2.初始化对象
3.将对象指向分配好的地址空间(执行完之后就不再是null了)
其中第2,3步在一些编译器中为了优化单线程中的执行性能是可以重排的。重排之后就是这样的:
1.分配内存空间
3.将对象指向分配好的地址空间(执行完之后就不再是null了)
2.初始化对象
重排之后就有可能出现上边分析的情况:
那么既然这个方式不能保证线程安全,那我们之间加上同步不就可以了吗?这确实也是一种方法
public class Singleton {
//私有的构造函数
private Singleton() {
}
//私有的静态变量
private static Singleton single =null;
//公有的同步静态方法
public synchronized static Singleton getInstance() {
if (single == null) { //line1
single=new Singleton(); //line2
}
return single;
}
}
这里和线程不安全的懒加载方式就是多了一个synchronized关键字,保证了线程安全,但是这又带来了另外一个问题,性能问题。如果,有多个线程会频繁调用getInstance()方法的话,可能会造成很大的性能损失。每次调用都需要同步,这会造成不必要的同步开销,而大不部分时候我们是用不到同步的。极其不推荐这种形式。那么为了解决这个问题,有人提出了我们非常熟悉的双重检查锁定(简称DCL)。
public class Singleton {
//私有的构造函数
private Singleton() {
}
private static Singleton single = null;
public static Singleton getInstance() {
if (single == null) {//第一次检查
synchronized (Singleton.class) {
single = new Singleton(); //第二次检查
}
}
return single;
}
}
这个问题和上边介绍过的重排问题一样。还是举ThreadA和ThreadB的例子:
当Thread经过第一次检查对象为null时,会接着去加锁,然后去执行new Singleton(),上边已经分析过了,改步骤存在重排现象,如果发生重排,即mInstance分配了内存地址,但是很没有完成初始化工作,而此时ThreadB,刚好执行第一次检查(没有加锁),mInstance已经分配了地址空间,不再为null,那么ThreadB会获取到没有完成初始化的mInstance,这就是DCL失效问题。
当然方法还是有的,那就是volatile关键字private volatile static Singleton singleton;
。
在JDK1.5之后使用volatile关键字,将禁止上文中的三步操作重排,既然不会重排,也就不会出现问题了。
问题是解决了,但是volatile要在JDK1.5以上版本(JDK1.5之前的可以参考http://www.ibm.com/developerworks/cn/java/j-dcl.html)才能起作用,其还会屏蔽jvm做的代码优化,这些有可能导致程序性能降低,并且目前为止DCL已经有一些复杂了。有没有更简单的方法呢?答案是有的
/**
* 静态内部类方式实际上是结合了饿汉式和懒汉式的优点的一种方式
*/
public class Singleton {
//私有的构造函数
private Singleton() {
}
/**
* 在调用getInstance()方法时才会去初始化mInstance
* 实现了懒加载
*
* @return
*/
public static Singleton getInstance() {
return SingletonInnerHolder.mInstance;
}
/**
* 静态内部类
* 因为一个ClassLoader下同一个类只会加载一次,保证了并发时不会得到不同的对象
*/
public static class SingletonInnerHolder {
public static Singleton mInstance = new Singleton();
}
}
这是一个很聪明的方式,结合了结合了饿汉式和懒汉式的优点,并且也不影响性能。为什么这么说?因为我们在单例类SingletonInner类中,实现了一个static的内部类SingletonInnerHolder,该类中定义了一个static的SingletonInner类型的变量mInstance,并且会在classLoader第一次加载SingletonInnerHolder这个类时进行初始化。这样做的好处是在classLoader在加载单例类SingletonInner时不会初始化mInstance。只有在第一次调用SingletonInner的getInstance()方法时,classLoader才会去加载SingletonInnerHolder,并初始化mInstance,并且由于ClassLoader的机制,一个ClassLoader同一个类,只加载一次,那么不管多少线程,得到的也是同一个类,保证了并发下是该方式是可用的。其缺点也是有的,有些语言不支持这种语法。
接下来在介绍一种很简单的方式:
public enum SingletonEnum {
SINGLETON_ENUM;
private SingletonEnum() {
}
}
就是这么的简单,改方式不仅能避免多线程并发同步的问题,而且还天生支持序列化,可以防止在反序列化时创建新的对象。是一种比较推荐的方式,在java中需要在JDK1.5以上才支持enum。
总结:单例模式还有其他的实现方法,熟悉Android的同学都知道,Handler机制中用到的ThreadLocal其实就使用了一种单例,就是在处理并发时,保证每一个线程都有一个单例实现。在上述介绍的各种方式中,没有哪一个是绝对最好的,需要结合各自的情况决定。例如一般不要求懒加载的话,可以使用写法一饿汉式,如果要求懒加载,如果明确需要懒加载的,再根据是否需要线程安全考虑选择写法二,三。如果单例类需要反序列化,那么可以使用写法六枚举。总之,需要结合自己的实际情况来看。最后,再来看看几个问题:
第一 、多ClassLoder情况,如果是多个ClassLoder都加载了单例类,那么就会出现多个同名的对象,这违背了单例模式的原则。解决这个问题,就要保证只有一个ClassLoder加载单例类。
第二、单例类序列化问题,只要保证反序列化时,得到同一个对象就可以了,通过重写readResolve()方法可以实现。
public class Singleton implements java.io.Serializable {
...
private Object readResolve() {
return mInstance;
}
}
除了上述几种常见的实现单例的方式,还有另一类的实现,代码如下:
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<String,Object>();//使用HashMap作为缓存容器
private Singleton() {
}
public static void registerService(String key, Objectinstance) {
if (!objMap.containsKey(key) ) {
objMap.put(key, instance) ;//第一次是存入Map
}
}
public static ObjectgetService(String key) {
return objMap.get(key) ;//返回与key相对应的对象
}
}
在程序的初始,将多种单例模式注入到一个统一的管理类中,在使用时根据key获取对应类型的对象。
在Android源码中,APP启动的时候,虚拟机第一次加载该类时会注册各种ServiceFetcher,比如LayoutInflater Service。将这些服务以键值对的形式存储在一个HashMap中,用户使用时只需要根据key来获取到对应的ServiceFetcher,然后通过ServiceFetcher对象的getService函数获取具体的服务对象。当第一次获取时,会调用ServiceFetcher的creatService函数创建服务对象,然后将该对象缓存到一个列表中,下次再取时直接从缓存中获取,避免重复创建对象,从而达到单例的效果。Android中的系统核心服务以单例形式存在,减少了资源消耗。
总结:不管以哪种形式实现单例模式,它们的核心原理是将构造函数私有化,并且通过静态公有方法获取一个唯一的实例,在这个获取的过程中必须保证线程的安全,同时也要防止反序列化导致重新生成实例对象。
参考: https://blog.csdn.net/chenkai19920410/article/details/54612505?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase