Java单例的最佳实现

传统的方式

单例模式(Singleton)的目的是为了保证在一个进程中,某个类有且仅有一个实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
// 静态字段引用唯一实例:
private static final Singleton INSTANCE = new Singleton();

// 通过静态方法返回实例:
public static Singleton getInstance() {
return INSTANCE;
}

// private构造方法保证外部无法实例化:
private Singleton() {
}
}

遗憾的是,这种写法在多线程中是错误的,在竞争条件下会创建出多个实例。必须对整个方法进行加锁:

1
2
3
4
5
6
public synchronized static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}

更好的方式

利用Java的enum,因为Java保证枚举类的每个枚举都是单例,所以我们只需要编写一个只有一个枚举的类即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum World {
// 唯一枚举:
INSTANCE;

private String name = "world";

public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}
}

枚举类也完全可以像其他类那样定义自己的字段、方法,这样上面这个World类在调用方看来就可以这么用:

1
String name = World.INSTANCE.getName();

使用枚举实现Singleton还避免了第一种方式实现Singleton的一个潜在问题:即序列化和反序列化会绕过普通类的private构造方法从而创建出多个实例,而枚举类就没有这个问题。

深入了解

枚举类似类,一个枚举可以拥有成员变量,成员方法,构造方法。

先来看枚举最基本的用法:

1
2
3
enum MyType{
A,B;
}

创建enum时,编译器会自动为我们生成一个继承自java.lang.Enum的类,我们上面的enum可以简单看作:

1
2
3
4
class MyType extends Enum{
public static final MyType A;
public static final MyType B;
}

对于上面的例子,我们可以把MyType看作一个类,而把A,B看作类的MyType的实例。
当然,这个构建实例的过程不是我们做的,一个enum的构造方法限制是private的,也就是不允许我们调用。

方法和实例方法

在enum中,我们可以定义类和实例的变量以及方法。

看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
enum MyType{
A,B;

static int value;
public static int getValue() {
return value;
}

String type;
public String getMyType() {
return type;
}
}

在原有的基础上,添加了类方法和实例方法。我们把MyType看做一个类,那么enum中静态的域和方法,都可以视作类方法。

和我们调用普通的静态方法一样,这里调用类方法也是通过 MyType.getValue()即可调用,访问类属性也是通过MyType.value即可访问。
下面的是实例方法,也就是每个实例才能调用的方法。那么实例是什么呢?

没错,就是A,B。所以我们调用实例方法,也就通过 MyType.A.getMyType()来调用就可以了。
最后,对于某个实例而言,还可以实现自己的实例方法。

再看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum MyType{
A{
public String getMyType() {
return "I will not tell you";
}
},
B;
static int value;

public static int getValue() {
return value;
}

String type;
public String getMyType() {
return type;
}
}

这里,A实例后面的{…}就是属于A的实例方法,可以通过覆盖原本的方法,实现属于自己的定制。
除此之外,我们还可以添加抽象方法在enum中,强制AB都实现各自的处理逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum MyType{
A{
public String getMyType() {
return "A";
}
},
B {
@Override
public String getMyType() {
return "B";
}
};

public abstract String getMyType();
}

把现有的类变单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//原本的类
class Resource{
}

//单例化
public enum ResourceSingle {
INSTANCE;
private Resource instance;
ResourceSingle() {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}

上面的类Resource是我们要应用单例模式的资源,具体可以表现为网络连接,数据库连接,线程池等等。
获取资源的方式很简单,只要 ResourceSingle.INSTANCE.getInstance() 即可获得所要实例。

下面我们来看看单例是如何被保证的:
首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。
也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。
可以看到,枚举实现单例还是比较简单的,除此之外我们再来看一下Enum这个类的声明:

1
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable

可以看到,枚举也提供了序列化机制。

某些情况,比如我们要通过网络传输一个数据库连接的句柄,会提供很多帮助。
最后借用 《Effective Java》一书中的话,

单元素的枚举类型已经成为实现Singleton的最佳方法。