单例模式在Android中算是很常用的一类模式了,当我们需要整个软件中有且只有一个实例对象时,我们可以写一个单例类。
最简单的单例
public class RecycleBin {
private static RecycleBin INSTANCE;
private RecycleBin() { }
public static RecycleBin getInstance() {
if (INSTANCE == null) {
INSTANCE = new RecycleBin();
}
return INSTANCE;
}
这样的代码大家肯定都不陌生,如果INSTANCE还没有实例化,那么实例化它,并且构造方法是private的,防止客户端new一个实例。这样的代码在单线程环境下可以较好的工作,但是在多线程环境下就会出现错误。假如线程A执行if (INSTANCE == null)
这一步后被挂起,线程B切换进来执行了这段代码,实例化了INSTANCE,此时INSTANCE就不为null了,之后把CPU重新让给了线程A,但此时A并不知道INSTANCE已被实例化这件事,又将INSTANCE指向了一个新的RecycleBin对象。
同步的单例模式
那么实现多线程下的单例模式呢。一个很粗暴的答案是:synchronized。是的,给getInstance方法加上这个关键字后,整个方法就是同步的了,可以实现单例模式。
public synchronized static RecycleBin getInstance() {
if (INSTANCE == null) {
INSTANCE = new RecycleBin();
}
return INSTANCE;
}
但是这么做会产生新的问题:效率很低。如果有多个线程要同时获得该对象,那么线程需要排队,一个一个获取,这会造成很大的浪费。
改进的单例模式
可以换个角度,因为是在检查INSTANCE == null
这一步代码上出现了问题,那么将锁加到这个代码段上即可,将代码修改为
public static RecycleBin getInstance() {
synchronized(RecycleBin.class) {
if (INSTANCE == null) {
INSTANCE = new RecycleBin();
}
}
return INSTANCE;
}
DCL(Double Check Locking)
因为synchronized的同步会产生大量的性能开销,追求性能的大佬发明了双重锁的办法来减小开销。
public static RecycleBin getInstance() {
if (INSTANCE == null) {
synchronized(RecycleBin.class) {
if (INSTANCE == null) {
INSTANCE = new RecycleBin();
}
}
}
return INSTANCE;
}
比起上面的代码,多了一道检测INSTANCE == null
的工序,这行代码可以避免除了第一次以后的同步。当对象已经实例化之后,就不会执行同步代码块了。但是这样的代码还是会存在问题,因为INSTANCE = new RecycleBin()
这一行代码不是原子操作。
下面来假设一个出错的场景
- 线程A执行了getInstance方法
- 线程A检查INSTANCE变量,发现为空
- 线程A执行
INSTANCE == new RecycleBin()
,将INSTANCE设置为了非空,但是在构造方法执行前被挂起了 - 线程B执行代码,检查INSTANCE变量,发现不为空,返回INSTANCE对象。(但此时INSTANCE还未调用构造方法)
实际上这一行代码被拆成了三步来执行
memory = allocate(); //#1为对象分配内存空间
init(memory); //#2初始化
instance = memory; //#3设置instance,将其指向刚分配的内存空间。
在某些编译器上,2和3会出现倒序,也就是类的域无法得到初始化,从而拿到一个并不正确的对象。
庆幸的是再Java1.5之后的版本可以给INSTANCE加上volitate关键字来避免编译器的优化,拿到正确的对象。
饿汉式
上面的几个方法都是属于懒汉式的方法,即等需要了再去实例化
。接下来介绍另一种叫做饿汉式:在类加载时就初始化对象。
public class RecycleBin {
private static RecycleBin INSTANCE = new RecycleBin();
private RecycleBin() { }
public static RecycleBin getInstance() {
if (INSTANCE == null) {
INSTANCE = new RecycleBin();
}
return INSTANCE;
}
因为静态域只会加载一次,所以产生的单例对象是安全的。
内部类
public class Singleton {
// 获得对象实例的方法
public static Singleton getSingleton() {
return SingletonHolder.instance;
}
/**
* 静态内部类与外部类的实例没有绑定关系,而且只有被调用时才会
* 加载,从而实现了延迟加载
*/
private static class SingletonHolder {
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static Singleton instance = new Singleton();
}
private Singleton() {
}
}
枚举
public enum Singleton {
// 定义枚举元素,他就是Singleton的一个实例
INSTANCE;
public void doSomething() {
// do something
}
}
使用枚举可以说是最佳的单例实践方式,因为即便构造器是私有的,仍然可以通过反射来调用私有构造器如
public class TestMain {
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
Class<?> classType = Singleton.class;
Constructor<?> c = classType.getDeclaredConstructor(null);
c.setAccessible(true);
Singleton singleton1 = (Singleton) c.newInstance();
Singleton singleton2 = Singleton.getSingleton();
System.out.println(singleton1 == singleton2);
}
}
另外还有一种特殊情况是反序列化,反序列化并不是通过调用构造器来构造对象的,反序列化操作提供了readSolve方法来重建对象,如果我们要避免反序列化时产生新的对象需要复写这个方法
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
而枚举帮我们完成了这些工作
小结
这部分也只是看书看了个大概,对多线程环境下单例的各种坑并不了解,这部分同时涉及了Java中类的加载机制,多线程,堆,栈等概念。DCL的错误的那部分也看的不是很懂。。这样一个简单的设计模式都有这么多花样,还要学习一个啊。
参考资料
Android设计模式解析与实战
单例模式各版本的原理与实践
Java线程安全兼谈DCL