单例设计模式是各种设计模式中最简单的,但是实际编码过程中使用最多的模式;面试中也经常被问到。我们来review一下单例设计模式
饿汉模式
public class Apple {
private static Apple instance = new Apple();
private Apple(){}
public static Apple getInstance(){
return instance;
}
}
饿汉模式会在类加载的时候就初始化实例,而非使用时
懒汉模式
public class Apple {
private static Apple instance = null;
private Apple(){}
public static Apple getInstance(){
if(instance==null){
instance = new Apple();
}
return instance;
}
}
懒汉模式下,实例会在使用时再去初始化;但是这种懒汉模式有线程安全问题,多线程情况下可能被创建多个实例。
线程安全的懒汉模式
public class Apple {
private static Apple instance = null;
private Apple(){}
public static synchronized Apple getInstance(){
if(instance==null){
instance = new Apple();
}
return instance;
}
}
上面的带锁的懒汉模式解决了线程安全的问题,但是效率不高,每次只允许一个线程获取实例。
双重校验的懒汉模式
public class Apple {
private static Apple instance = null;
private Apple(){}
public static Apple getInstance(){
if(instance==null){ //第1次检查
synchronized (Apple.class){
if(instance==null) //第2次检查
instance = new Apple();
}
}
return instance;
}
}
第1次检查在synchronized外面,然后同步锁住代码块;第2次检查是为了防止在初始实例化的时候,线程B在同步块等待,线程A已经进入同步块并初始化了实例,等A退出同步块,线程B进入同步块不需要在初始实例。
上述双重校验还是有问题,原因是new Apple()并不是原子操作,实际上是分成3个步骤。
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
由于这三个步骤可能按照1-3-2来执行,当3执行完了instance就非null了,这时线程B执行getInstance就会获得对象并使用就会报错。具体的解释参考左耳朵耗子的博文
双重校验版本2
public class Apple {
private static volatile Apple instance = null;
private Apple(){}
public static Apple getInstance(){
if(instance==null){
synchronized (Apple.class){
if(instance==null)
instance = new Apple();
}
}
return instance;
}
}
使用volatile主要目的是禁止指令重排序,让new Apple()按照1-2-3的步骤执行。这样就能防止上述双重校验版本1的问题
静态内部类方法
public class Apple {
private Apple(){}
public static Apple getInstance(){
return AppleHolder.instance;
}
public static class AppleHolder{
private static Apple instance = new Apple();
}
}
静态内部类的方法,是JVM机制保证线程安全;当多个线程同时getInstance时,AppleHolder类的加载是JVM加载的,不会有线程问题;又是懒汉模式在需要时初始化。
总结
单例设计模式,有4中方式:饱汉式、懒汉式、双重校验方式、静态内部类方式。其中双重校验方式还有优化版本,在实际变成开发中推荐使用静态内部类的方式。