设计一个类,只能生成该类的一个实例。
在第一次调用获取实例方法时分配内存,实现了懒加载。但是当有多个线程并行调用 getInstance() 的时候,就会创建多个实例,也就是说在多线程下不能正常工作。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
也可以对 getInstance() 方法里的代码块设为同步。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
使用 synchronized 虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。synchronized 修饰的同步方法比一般方法要慢很多,如果多次调用 getInstance(),累积的性能损耗就比较大了。此外,由于在任何时候只能有一个线程调用 getInstance() 方法,但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance ;
}
}
可以看到上面在同步代码块外多了一层 instance 为空的判断。由于单例对象只需要创建一次,如果后面再次调用 getInstance() 只需要直接返回单例对象。因此,大部分情况下,调用 getInstance() 都不会执行到同步代码块,从而提高了程序性能。
但是,这里还有一个隐患,主要是因为 instance = new Singleton() 这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将 instance 对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
这里要提到 Java中 的指令重排优化。所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。
由于指令重排优化的存在,导致初始化 Singleton 和将对象地址赋给 instance 的顺序是不确定的。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
不过还好在 JDK1.5 及之后版本增加了 volatile 关键字。volatile 的一个语义是禁止指令重排序优化,也就保证了 instance 变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance ;
}
}
因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
但是它的缺点也很明显,它不是一种懒加载模式。即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。
class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
与饿汉式一样,这种方式也是利用了类加载机制来保证只创建一个 instance 实例,因此不存在多线程并发的问题,是线程安全的。
此外,它是在内部类里面去创建对象实例,只要应用中不使用内部类,JVM 就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。
class Singleton {
private static class SingletonHolder {
public static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
public enum Singleton {
INSTANCE;
}
通过 Singleton.INSTANCE 就可以获取到单例对象。
枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。
一般情况下直接使用饿汉式就好了,如果明确要求要懒加载可以使用静态内部类,如果涉及到反序列化创建对象时可以使用枚举的方式来实现单例。