实现单例模式主要有如下几个关键点:
1. 构造函数不对外开放,一般为private。(私有构成函数)
2. 通过一个静态方法或枚举返回单例类对象。(提供外部获取实例的方法)
3. 确保单例类的对象有且只有一个,尤其是在多线程环境下。(多线程安全问题)
4. 确保单例类对象在反序列化时不会重新创建对象。
一、单例模式的几种创建方式
1. 饿汉方式(会默认创建实例)
public class Singleton {
private static Singleton mInstance = new Singleton();
private Singleton() {}
private static Singleton getInstance() {
return mInstance;
}
}
2. 懒汉方式(只有在第一次使用时才会创建实例,比饿汉利用率高)
先看一种非线程安全的,如果不是在多线程中访问,可以使用
private class Singleton {
private static Singleton mInstance;
private Singleton() {}
public static Singleton getInstance() {
if (mInstance == null) {
mInstance = new Singleton();
}
return mInstance;
}
}
下面看下Double Check Lock实现方式,也是使用比较广的
private class Singleton {
private static Singleton mInstance;
private Singleton() {}
public static Singleton getInstance() {
if (mInstance == null) {
synchronized (Singleton.class) {
if (mInstance == null) {
mInstance = new Singleton();
}
}
}
return mInstance;
}
}
下面简单分析下这种方式,来看核心代码mInstance = new Singleton();
这里是一句代码,但实际上它并不是原子操作,这句代码会被编译成多条汇编指令,具体如下
(1) 给Singleton的实例分配内存
(2) 调用Singleton()的构造函数,初始化成员字段
(3) 将mInstance对象指向分配的内存空间(此时mInstance就不是null了)
由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM(Java Memory Model,即Java内存模型)中Cache、寄存器到主内存回写顺序的规定,上面的(2)和(3)的顺序无法保证。
当线程A执行顺序1-3-2,当执行到3还没到2,这时线程B开始执行,mInstance已经非空,线程B直接取走mInstance,再使用时就会出错,这就是DCL失效问题。
JDK1.5后可以增加volatile关键字,来保证,但会牺牲性能。
关于volatile关键字的使用,参考http://www.cnblogs.com/dolphin0520/p/3920373.html
private static volatile Singleton mInstance;
DCL模式是使用最多的单例实现方式,除非在并发场景比较复杂或低于JDK6版本使用,否则,这种方式一般都能够满足需求。
3. 静态内部类方式
为了解决DCL失效的问题,建议使用如下方式替换:(缺点是无法给构造函数提供参数)
因为java机制规定,内部类SingletonHolder只有在getInstance()方法第一次调用的时候才会被加载(实现了lazy),而且其加载过程是线程安全的(实现线程安全)。内部类加载的时候实例化一次instance。
private class Singleton {
private Singleton() {}
private static Singleton getInstance() {
return SingletonHolder.mInstance;
}
private static class SingletonHolder {
private static final Singleton mInstance = new Singleton();
}
}
上面3种方式,在反序列化时,会出现重新创建对象的情况。
通过序列化可以将一个单例的实例对象写到磁盘,然后再读回来,从而有效地获得一个实例,即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例。
可以通过实现提供的函数来控制对象的反序列化
private Object readResolve() throws ObjectStreamException {
return mInstance;
}
4. 枚举方式(可以防止反序列化创建对象)
public enum Singleton {
INSTANCE;
}
5. 使用容器方式
书中还介绍了一种另类的实现,具体如下
public class SingleManager {
private static Map<String, Object> objMap = new HashMap<String, Object>();
private SingleManager() {}
public static void registerService(String key, Object instance) {
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}
public static Object getService(String key) {
return objMap.get(key);
}
}
个人理解,这种只是保存单例对象的一种方式,因为还是需要使用registerService将单例对象注册,即还是需要在外部先创建个单例对象。
二、利用单例模式改造书中的图片加载框架
上节说过,原来的图片加载没有使用单例模式,每个使用者都会创建一个实例,就导致会创建多个缓存对象,分配大量内存,这在Android是很严重的问题,会导致内存不够,继而OOM。
package com.outman.myimageloader;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.widget.ImageView;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by xingjingmin on 16/12/15.
*/
public class ImageLoader {
// 封装的缓存类
private ImageCache mImageCache = new MemoryCache();
// 线程池,线程数量为CPU的数量
private ExecutorService mExecutorService;
private static ImageLoader mInstance;
private ImageLoader() {
mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
}
public static ImageLoader getInstance() {
if (mInstance == null) {
synchronized (ImageLoader.class) {
if (mInstance == null) {
mInstance = new ImageLoader();
}
}
}
return mInstance;
}
................
}
三、Android源码中的单例模式 LayoutInflater
获取LayoutInflater对象有两种方式
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LayoutInflater inflater = LayoutInflater.from(context);
其中第二种方式,也是调用的第一种方式创建的。
我们知道Context的具体实现类是ContextImpl(可以参考源码或图书)
在ContextImpl类中,在虚拟机第一次加载该类时会注册各种ServiceFatcher,其中包括LayoutInflater Service。并将这些服务以键值对的形式存储在一个HashMap中,用户使用时只需根据key来获取对应的ServiceFatcher,然后通过ServieFatcher对象的getService方法创建服务对象,从而达到单例的效果。
下面再简单描述下LayoutInflater是如何解析layout文件,创建View的。
整个layout文件,其实就是一个树结构,通过XmlPullParser利用深度优先遍历方式,每解析到一个View元素就会递归,直到这条路径的最后一个元素都解析完毕,再回溯过来将每个View元素添加到他们的parent中。
其中有个知识点,一般系统的View,在layout中我们是不写完整路径的,比如TextView,Button等,而自定义控件是要全路径的,比如<com.dp.custom.View/>,这是因为Android系统在解析layout时,会默认给系统添加前缀“android.widget.”也就是系统是根据完整路径构造View的。系统是通过是否包含"."来判断的。
总结:
优点,简单说就是只有一个实例,减少性能开销
缺点,如果持有Context,容易引发内存泄漏,最好使用ApplicationContext。
个人一些观点,有的开发者,喜欢将数据保存到单例对象中,给多个界面共享使用,这种处理不好,很可能导致数据不使用时没有及时清除数据,导致一直占有内存。