Java设计模式 - 单例模式

单例模式是一种很常见的设计模式,我们在各种第三方库的源码中经常可以看见它的身影。单例的本质是控制实例的数量,全局有且只有一个对象,并且能够全局访问。虽然它很“小”,但作用却很大。例如可以节约系统内存资源,防止一些不必要的操作。 但凡做Java相关的编程工作,都应该要了解Java中的单例是怎么写的,常见的有几种写法,特点是什么。常见的有下面几种:

常见写法

这应该是最简单的写法,也叫懒汉式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {

private static Singleton mInstance;

private Singleton() {
}

public static Singleton getInstance() {
if (mInstance == null) {
mInstance = new Singleton();
}
return mInstance;
}
}

但是这种写法有一个问题就是当多个线程同时调用getInstance方法的时候,就会产生多个实例,这样线程就会变得不安全了。

既然存在多个线程同时调用这个问题,所以我们会想到synchronized关键字似乎可以解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {

private static Singleton mInstance;

private Singleton() {
}

public static synchronized Singleton getInstance() {
if (mInstance == null) {
mInstance = new Singleton();
}
return mInstance;
}
}

synchronized同步锁可以保证这个方法只能同时被一个线程占有,这样看来好像是可以防止多个线程同时调用而产生多个实例这个问题,但是也有副作用。我们的目的其实只是在第一个初始化mInstance的时候需要锁上,而后面取用mInstance的时候,根本不需要线程同步。上面的写法是每次调用getInstance获取实例的时候都会进行同步锁检查,这无疑降低了效率,同时增加了资源的消耗。

双重检查锁写法

既然上面的写法在每次getInstance的时候都检查锁,降低了效率消耗了大量资源。于是就产生了双重检查锁写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public 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的那部分代码,这样看起来好像可以了。但是,整个流程是这样的,首先给mInstance分配内存,然后调用构造方法进行初始化操作,最后才将mInstance对象指向分配的内存空间。要是初始化操作的时候花费了大量的时间,这里的顺序如果在此时被打断的话,那么mInstance对象将指向为完成初始化的mInstance的内存空间,这时就会报错了。这时,volatile关键字就登场了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Singleton {

//添加volatile关键字修饰
private volatile static Singleton mInstance;

private Singleton() {
}

public static Singleton getInstance() {
if (mInstance == null) {
//保证了同一时间只能只能有一个对象访问此同步块
synchronized (Singleton.class) {
if (mInstance == null) {
mInstance = new Singleton();
}
}
}
return mInstance;
}
}

用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的值,相当于自动发现最新值。volatile也相当于一个微型的锁,执行的顺序就不会被打乱了,mInstance对象也不会指向未完成初始化的mInstance的内存空间,最终完成单例模式的实现。

这种方法是比较推荐用的,很多开源库都是用这种方法,我用得最多的也是这种方法。

饿汉式写法

代码如下:

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {

private Singleton() {
}

private static Singleton mInstance = new Singleton();

public static Singleton getInstance() {
return mInstance;
}
}

饿汉式特点是提前实例化,没有懒汉式中多线程问题,但不管我们是不是调用getInstance方法都会存在一个实例在内存中。这样会延长类的加载时间,不能传递参数,并且占用资源,造成浪费。

静态内部类写法

这是在《Effective Java》中推荐的一种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {

private Singleton() {
}

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}

}

和饿汉式有点像,但是在类加载的时候并没有初始化,只是在调用getInstance方法的时候才会从静态内部类中加载。而且没有多线程的问题,也是一种推荐的写法。

枚举写法

《Effective Java》中还看到了一种比较少见的写法,枚举:

1
2
3
4
5
6
7
8
9
10
11
public enum Singleton {

INSTANCE;

Singleton() {
}

public void doSomething() {
//do something...
}
}

使用的时候:

1
Singleton.INSTANCE.doSomething();

因为枚举的特性,保证了只有一个实例,而且自由序列化,线程安全。所以,也可以用枚举的方法写单例模式。

总结

我们比较了几种常见的写法,最终推荐是用双重检查锁写法和静态内部类的写法。枚举虽然也可以,但是在Android开发中几乎没见过,而且Google的Android官网不建议使用enums,因为占用内存多。所以如果是Android开发,还是用双重检查锁写法或者静态内部类的写法吧。

Share