1、使用场景
所谓单例模式,就是只可能存在唯一一个类的实例,不能再多了。单例模式可以说是Java设计模式中最简单但也是最常用的一种了。在Android开发中也同样如此,当App需要一个全局的、跟Application同生命周期的服务或者需要统一管理调度某种资源等情况,我们通常会编写一个单例实现的类,达到统一资源服务入口的目的,同时也可以减少不必要的资源开销,提升App性能。Android源码中,也有很多单例模式的使用。因此,学习好单例模式非常重要。
2、实现方式
单例模式有很多种实现方式,基本思想是内部提供一个static方法返回类的一个唯一实例,同时只设计一个private类型构造函数,屏蔽外部创建类实例的入口,以此保证类的实例最多只有一个。下面简单总结分析下。
- 饿汉模式
public class SingletonTest {
private SingletonTest (){}
private final static SingletonTest instance = new SingletonTest();
public static SingletonTest instance(){
return instance;
}
}
利用Java语言的特性,static类型变量在类加载时就会被初始化,从而保证了有且只有一个类实例。但是,这种写法可能会造成资源浪费,因为即使程序中没有使用到该类方法,也会创建该类实例;并且,如果该类初始化加载操作过多,也会延长程序的启动,影响用户体验。
- 懒汉模式
public class SingletonTest {
private SingletonTest (){}
private static SingletonTest instance;
public static SingletonTest instance(){
if (instance == null) {
instance = new SingletonTest();
}
return instance;
}
}
这种写法解决了上面的问题,在第一次使用该类时,才会去创建该类实例。但是,却引入了另一个问题,在多线程环境下,并不能保证只有一个类实例。于是很自然有了下面这种写法。
- 线程安全模式
public class SingletonTest {
private SingletonTest (){}
private static SingletonTest instance;
public static synchronized SingletonTest instance(){
if (instance == null) {
instance = new SingletonTest();
}
return instance;
}
}
synchronized关键字很好的保证了多线程环境下也能只有一个类实例,但是在高并发情况下效率却是很低的,每次获取类实例都得锁住整个类,严重影响其他线程的操作。我们希望只有在实例没有创建时才去加锁保证只创建一次,其他情况下,应该直接返回类实例即可,没有必要再加锁,提升性能。
- Double-Checked Locking
public class SingletonTest {
private SingletonTest (){}
private static SingletonTest instance;
public static SingletonTest instance(){
if (instance == null) {
synchronized (SingletonTest.class){
if(instance == null) {
instance = new SingletonTest();
}
}
}
return instance;
}
}
Double-Checked Locking写法可以说基本做到了上面的要求。但是,这还不够完美。instance = new SingletonTest()这条语句并不是原子性的,大体包括三个操作:
(1)为new操作分配内存空间
(2)执行构造函数,创建SingletonTest类实例
(3)将instance引用指向分配的内存地址
JVM或者Android 的Dalvik VM并不保证上述(2)、(3)两步操作的顺序,也就是说有可能instance不为null了,但是指向的内存对象还未完全初始化成功,这时其他线程直接取走instance使用就有可能出现问题。在Java5之后,可以使用volatile关键字,保证instance每次都从主内存读取,从而一定程度上保证单例实现的正确性。
- 静态内部类实现
public class SingletonTest {
private SingletonTest (){}
public static SingletonTest instance(){
return InnerClass.instance;
}
private static class InnerClass{
private final static SingletonTest instance = new SingletonTest();
}
}
静态内部类中的instance实例并不会在SingletonTest类加载时初始化,而是只有在InnerClass第一次使用时才去创建,类似于懒汉模式延迟了加载,但是也保证了多线程安全,是一种比较优秀的实现方式。
- 枚举实现
public enum SingletonTest {
INSTANCE;
}
简单几行代码便解决了问题,并且不会出现多线程安全问题。经典的《Effective Java》一书中推荐这种实现方式。
3、进阶
上述单例的各种实现,真的就能保证只存在唯一实例了吗?我们考虑以下两种情况。
- Java反射实例化
作为一名Java程序员,Java自带的黑科技——反射,应该派上用场了。使用如下代码,对上面几种单例实现(除了枚举实现)逐一进行测试。
public static void main(String[] args) throws Exception {
Constructor c = SingletonTest.class.getDeclaredConstructor();
c.setAccessible(true);
SingletonTest test = (SingletonTest) c.newInstance();
System.out.println(instance() == test);
}
不出所料,输出结果均为false,表明我们创建了唯一实例之外类的另一个实例。
- Java序列化反序列化
Java序列化是将object转换成字节序列,反序列化是将字节序列恢复成内存对象。通常反序列化会重新创建类实例,即使是单例类。
public class SingletonTest implements Serializable {
private SingletonTest (){}
public static SingletonTest instance(){
return InnerClass.instance;
}
private static class InnerClass{
private static SingletonTest instance = new SingletonTest();
}
public static void main(String[] args) throws Exception {
// 序列化
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("file.txt"));
outputStream.writeObject(instance());
// 反序列化
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("file.txt"));
SingletonTest test = (SingletonTest) inputStream.readObject();
System.out.println(test == instance());
}
}
输出结果仍然为false,同反射一样,反序列化也重新创建了类实例。但是,不同于对于反射攻击的无计可施,我们可以使用readResolve方法指定反序列化返回的对象,从而保证单例的唯一性。例如:
private Object readResolve() throws ObjectStreamException {
return instance();
}
需要注意的是上面的讨论都避开了枚举实现方式,为什么呢?借用《Effective Java》里的一段关于枚举实现的解释。
无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。