单例模式

单例模式

1. 饿汉式

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

优点:没有线程安全问题
缺点:在初始化时就创建好了,浪费内存空间

2. 懒汉式

2.1 线程不安全

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

优点:只有当用的时候才检查是否有实例,没有才创建
缺点:有线程安全与不安全两种,区别在于是否有 synchronized 关键字

2.2 线程安全

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

3. DCL

由于线程安全的懒汉式的 synchronized 是加在方法上的,如果该方法里还有其他的一些代码,会降低执行效率,加锁的粒度太粗,所以可以进而改写下面这个

3.1 懒汉式优化后的一种写法

1
2
3
4
5
6
7
8
9
10
11
12
public class LHan {
private static LHan instance;
private LHan(){}
public static LHan getInstance(){
if(instance == null){
synchronized(this){
instance = new LHan();
}
}
return instance;
}
}

但是这种写法也存在一定问题,当线程A在判断instance==null后停住了,此时还没有创建实例;线程B抢到了资源,发现instance==null,也会进入到代码块,于是A和B都会创建一个实例。

3.2 DCL 单例模式

Double Check Lock 两次检查,中间插入一个lock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LHan {
private static volatile LHan instance; // 必须要加 volatile,禁止指令重排序,其实就是禁止在 new LHan() 的时候禁止重排序
private LHan(){}
public static LHan getInstance(){
if(instance == null){ // 这一层判断是为了提高效率。锁竞争很耗费时间和效率,避免多个线程每一次getInstance都要进入到同步代码块
synchronized(this){
if(instance == null){
instance = new LHan();
}
}
}
return instance;
}
}

有可能会存在,A在第一层判断结束后停住,B进入同步代码块,new一个对象实例,进行一定操作后,把instance置为null,然后A进入同步代码块,会再次new一个对象。解决:添加版本号。

3.2.1 关于重排序的一个解答
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
private static Singleton instance = null;
public static Singleton getInstance() {
if(null == instance) { // 线程二检测到instance不为空
synchronized (Singleton.class) {
if(null == instance) {
instance = new Singleton(); // 线程一被指令重排,先执行了赋值,但还没执行完构造函数(即未完成初始化)
}
}
}
return instance; // 后面线程二执行时将引发:对象尚未初始化错误
}
}

如上代码段中的注释:假设线程一执行到instance = new Singleton()这句,这里看起来是一句话,但实际上其被编译后在JVM执行的对应会变代码就发现,这句话被编译成8条汇编指令,大致做了三件事情:

  1)给instance实例分配内存;

  2)初始化instance的构造器;

  3)将instance对象指向分配的内存空间(注意到这步时instance就非null了)

  如果指令按照顺序执行倒也无妨,但JVM为了优化指令,提高程序运行效率,允许指令重排序。如此,在程序真正运行时以上指令执行顺序可能是这样的:

  a)给instance实例分配内存;

  b)将instance对象指向分配的内存空间;

  c)初始化instance的构造器;

  这时候,当线程一执行b)完毕,在执行c)之前,被切换到线程二上,这时候instance判断为非空,此时线程二直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化)。

  具体来说就是synchronized虽然保证了线程的原子性(即synchronized块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)。

  根据以上分析可知,解决这个问题的方法是:禁止指令重排序优化,即使用volatile变量。

4. 静态内部类

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

}

静态内部类的方式效果类似双检锁,但实现更简单。但这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。