单例模式,是我们最常用的设计模式之一,主要作用是保证在应用程序中,一个类Class只有一个实例存在。本篇文章主要是对常用的单例模式实现方式做一个总结。
饿汉模式是我们比较常用的一种实现单例模式的方式之一
public class Singleton {
public static int VALUE = 1;
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
Log.i("instance", "singleton constructor");
}
public static Singleton getInstance() {
return INSTANCE;
}
}
通过静态属性初始化一个INSTANCE
,提供getInstance()
获取INSTANCE
实例,而构造函数为无参私有函数,这样保证了实例在应用中只有一个实例(此处不考虑多个ClassLoader
分别对Singleton
进行加载)。
但是此种实现方式有一个很大的缺点,先看一段代码
private void singletonTest() {
int value = Singleton.VALUE;
Log.i("instance", "singleton value:" + value);
}
在singletonTest()
中访问Singleton.VALUE
的值,但没有调用Singleton.getInstance()
获取单例实例,看打印的log如下
I/instance: singleton constructor
I/instance: singleton value:1
在访问Singleton.VALUE
的时候,对Singleton
进行了初始化,因为对类进行加载的时候,会对类的静态属性进行初始化,从而对Singleton
进行实例化。
这样可能存在一个缺点:当Singleton
的实例化需要等待其他一些信息的加载时,而在信息没有加载完成的时候,调用了Singleton
的某个静态属性间接导致对Singleton
进行实例化,导致Singleton
实例异常,甚至会影响程序的正常运行。
当Singleton
的实例化不依赖于外部时,可以采用此种饿汉方式实现单例模式
上面介绍的饿汉模式在类进行加载的时候就对Singleton
进行了实例化,不能做到延时实例化;而为了能够做到延时实例化,只有在调用到getInstance()
的时候才进行实例化,下面的实现方式俗称懒汉模式,能够做到延时实例化
public class Singleton {
private static Singleton1 INSTANCE;
private Singleton() {
Log.i("instance", "singleton constructor");
}
public static Singleton getInstance() {
if(INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
在getInstance()
函数的里面进行判空处理,若为空,则进行初始化;但是这样的实现不是线程安全的,假如线程1执行到if(INSTANCE == null)
的时候,发现INSTANCE
为空,从而准备对Singleton
进行实例化;在线程1实例化未开始之前,INSTANCE
依旧为空,此时刚好有一个线程2也执行到if(INSTANCE == null)
,发现此时INSTANCE
为空,从而对Singleton
进行实例化,导致Singleton
不能保证只有一个实例。面对这种线程安全的问题,我们马上想到了同步,通过同步实现单例模式
public class Singleton {
public static Singleton INSTANCE;
private Singleton() {
Log.i("instance", "singleton constructor");
}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if(INSTANCE == null) {
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
通过synchronized
同步确实解决了上面懒汉模式中,因为线程安全可能会产生多个实例的问题,但是上面的实现代码还是会存在一些问题;当多个线程在执行getInstance()
的时候,因为synchronized
同步块,只有一个线程能访问其中的代码,其他的线程必须等候,当INSTANCE
已经实例化,不为空的情况下,每个线程都只会读取INSTANCE
,而不会进行实例化;但上面的代码都必须等待上一个获取锁的线程对INSTANCE
判断是否为空,释放锁之后,才能让其他的一个线程获取锁,进行INSTANCE
是否为空的判断,这会带来新的性能问题。
上面的实现方式在INSTANCE
不为空的时候会带来性能问题,而懒汉模式在INSTANCE
为空的时候会带来可能产生多个实例的问题,因此只需要在INSTANCE
为空的时候,进行synchronized
同步即可,这就是常用的实现单例模式方式之一的Double Check
.
public class Singleton {
public static Singleton INSTANCE = null;
private Singleton() {
Log.i("instance", "singleton constructor");
}
public static Singleton getInstance() {
if(INSTANCE == null) {
synchronized (Singleton.class) {
if(INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
上面的Double Check看上去确实没问题,但是真的没问题了吗?
在JAVA
中,JVM会对我们的代码进行优化,而其中之一就是指令重排序,非原子性的操作都可能会被JVM重排序,从而可能会带来新的问题。
INSTANCE = new Singleton()
是非原子性的操作,在JVM中分为三个步骤执行:
1.在内存中为INSTANCE
申请一块内存
2.通过构造函数Singleton()
对它的成员变量进行初始化 (相当于new Singleton()
)
3.将步骤2中实例化的对象分配到步骤1中的内存中 (相当于INSTANCE = new Singleton()
,此时开始INSTANCE
不为null
)
因为JVM可能对指令进行重排序,执行的步骤可能是 1 → 3 → 2 或者 2 → 1 → 3,当步骤3在步骤2之前执行时,执行完步骤3后INSTANCE
已经不为null
,但是因为还没执行步骤2,导致INSTANCE
中的一些成员变量还没有被初始化,此时若另外一个线程执行getInstance()
,发现INSTANCE
不为null
,直接返回了一些成员变量还没有被初始化的INSTANCE
,这样就可能因为这样INSTANCE
而导致一些不可预知的问题。
而JAVA
中的关键字volatile
能够禁止指令进行重排序,从而避免出现上述问题,最终的Double Check实现如下
public class Singleton {
public static volatile Singleton INSTANCE = null;
private Singleton() {
Log.i("instance", "singleton constructor");
}
public static Singleton getInstance() {
if(INSTANCE == null) {
synchronized (Singleton.class) {
if(INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
在饿汉模式中提到,当类被加载时,类的静态属性会被初始化,并且类一个只会被加载一次,如果我们专门定义一个类来实现饿汉模式,既能保证只会出现一个实例和延时加载,同时也能避免出现线程安全的问题,这就是我们常用的内部类方式实现单例模式
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这也是比较推崇的实现单例的方式之一
在Java引入枚举之后,可以通过枚举来实现单例模式
public enum Singleton{
INSTANCE;
}
枚举在编译之后会生成对应的类,对应的枚举值会生成为静态属性,在枚举类被加载的时候会生成对应的实例,因为类只会被加载一次,因此能够实现实现单例模式,但是是因为在类加载的时候进行实例化的,所以同同样存在饿汉模式存在的问题。
通过上面的分析,实现单例模式,推荐四种方式实现:饿汉模式、Double Check、内部类、枚举
饿汉模式 与 枚举方式 :在类被加载的时候进行实例化,不会延时初始化;在Singleton
实例若依赖于其他信息的加载时,不推荐使用这两种方式加载。
Double Check 与 内部类方式:在首次调用getInstance()
的时候进行加载,延时初始化;没有限制,适合所有场景使用。
其他实现单例模式的方式或多或少存在线程安全问题,不推荐使用
共同的问题
但是不管是饿汉模式、Double Check 、 内部类以及枚举的实现方式,都存在两个共同的问题:
1.反射:因为是私有构造函数,通过反射即可调用私有构造函数,实现多个实例
2.序列化:当把一个单例序列化之后再次反序列化就会得到一个新的对象,这样就实现了多个实例。
如何应对:在《Effect Java》中提到,为了维护并保证Singleton
,必须声明所有实例域(成员变量)都是瞬时(transient)的,并提供一个readResolve
方法。
private Singleton readResolve() {
return INSTANCE;
}