java 单例模式

单例模式算是设计模式中最容易理解,也是最容易手写代码的模式了吧。但是容易不一定意味着简单,想要用好、用对单例模式,还真得费一番脑筋。本文对Java中常见的单例模式写法做了一个总结,如有错漏之处,恳请读者指正。

懒汉式-非线程安全

1
2
3
4
5
6
7
8
9
class Singleton {
private static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
if (null == instance)
instance = new Singleton();
return instance;
}
}

这种方法可以实现延时加载,但是有一个致命弱点:线程不安全。如果有两条线程同时调用getInstance()方法,就有很大可能导致重复创建对象。

懒汉式-线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton {
private volatile static Singleton instance = null;
private Singleton() { }
public staitc Singleton getInstance() {
if (null == instance) {
synchronized (Singleton.class) {
if (null == instance) {
instance = new Singleton();
}
}
}
}
}

饿汉式

顾名思义,饿汉法就是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建。代码如下:

1
2
3
4
5
6
7
class Singleton {
private static instance = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return instance;
}
}

这样做的好处是编写简单,但是无法做到延迟创建对象。但是我们很多时候都希望对象可以尽可能地延迟加载,从而减小负载就需要懒汉式单例模式。

饿汉式-static final

这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

1
2
3
4
5
6
7
class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return instance;
}
}

这种写法如果完美的话,就没必要在啰嗦那么多双检锁的问题了。缺点是它不是一种懒加载模式,单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。
饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

静态内部类

1
2
3
4
5
6
7
8
9
class Singleton {
private Singleton() { }
private static class Holder {
private static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return Holder.instance;
}
}

这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

枚举

1
2
3
4
5
6
7
8
9
10
public enum Singleton {
INSTANCE;
private String name;
public String getName() {
return name;
}
public void setName() {
this.name = name;
}
}

使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,推荐尽可能地使用枚举来实现单例。

总结

除枚举外其他的实现都有两个共同的缺点:

  • 都需要额外的工作来实现序列化,否则反序列化时都会创建一个新的实例
  • 可能会有人使用反射强行调用我们的私有构造器

一般程序而言,一般直接使用饿汉式(静态常量)即可,如果需要懒加载倾向使用静态内部类,如果确实需要反序列化时可以试着使用枚举来实现;