单例

什么是单例模式

单例模式可以使一个类只有一个对象,能减少频繁创建对象的时间和空间开销。
单例模式需要注意的点

  1. 构造方法私有
  2. 如果是延迟加载,对象实例需要私有;
  3. 注意线程安全
单线程单例

单线程模式下一个典型的单例模式代码如下:

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

单线程下是没问题的,但多线程下会出现创建出多个实例的情况。

多线程单例

上面的代码无法保证线程安全,解决方法也很简单,加锁

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

加锁固然简单,但是也带来了性能的损耗,每次请求实例时都要请求锁,且如果两个线程同时请求获得对象实例时,要排队等待。
其实呢,为了保证线程安全,也就是保证不会出现多个实例,我们只要对instance = new SingleInstance2()加锁即可

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

但是这样呢有重新暴露出了线程安全的问题,比如线程A在代码1处判断为空后,资源让给线程B,线程B在1处同样判断为空后,资源返回给A,A获得锁,创建实例返回,B获得资源然后获得锁,同样还是会重新创建实例。怎么解决呢?就是在获得锁之后,重新进行一次空判断。

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

上面的代码看似完美,其实还是有隐藏问题,什么问题呢?
问题出在instance = new SingleInstance3()这行代码上,其实new一个新对象并不是一个原子操作,是分步的:

  1. memory=allocate()分配对象的内存空间。
  2. createInstance()初始化对象
  3. instance=memory 设置instance指向刚分配的内存
    大致来讲就是上面三步,但是由于java指令重排序优化的存在,会导致第二步和第三步没有绝对的先后顺序,在实例运行过程中,完全有可能出现1->3->2的顺序,那么表现在上面的代码中是什么情况呢:
    线程A执行到代码1处,先分配对象内存空间,再将instance指向刚分配的空间,然后资源让给线程B,线程B检查instance时候为空,判断结果是非空,直接返回使用,这时返回的是还未完全初始化好的实例,使用的话肯定会出错。解决办法是使用volatile修饰instance,volatile的重要作用就是禁止指令重排序,保证不会出现1->3->2的执行顺序,同时volatile保证在写之前会同步最新的数据

happens-before
volatile变量规则:这是一条比较重要的规则,它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。