先放代码:
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;
}
}
然后来分析getInstance()每一步的作用
第一个if语句,用来确认调用getInstance()时instance是否为空,如果不为空即已经创建,则直接返回,如果为空,那么就需要创建实例,于是进入synchronized同步块。
synchronized加类锁,确保同时只有一个线程能进入,进入以后进行第二次判断,是因为,
对于首个拿锁者,它的时段instance肯定为null,那么进入new Singleton()对象创建,
而在首个拿锁者的创建对象期间,可能有其他线程同步调用getInstance(),那么它们也会通过if进入到同步块试图拿锁然后阻塞。
这样的话,当首个拿锁者完成了对象创建,之后的线程都不会通过第一个if了,而这期间阻塞的线程开始唤醒,它们则需要靠第二个if语句来避免再次创建对象。
以上就是双检索的实现思路,synchronized与第二个if即是用来保证线程安全与不产生第二个实例,也是Double_Checked_Lock由来。
那么volatile的作用体现在哪呢?
一开始我认为是volatile的同步性,因为要在首个拿锁者创建对象以后,立即保证instance的可见性,以让被唤醒的阻塞线程能够在第二个if语句的时候得到instance非空的结果。
但这个可见性其实是用synchronized来保障的,并不需要volatile来多此一举了。
后来才知道应该是避免指令重排序,说明如下
这里的指令重排序主要体现在instance = new Singleton()这条语句上了。
这条语句显然是个复合操作,可以简单分下,(已完成类加载 ,假设在堆上分配内存)
1.在堆中分配对象内存
2.填充对象必要信息+具体数据初始化+末位填充
3.将引用指向这个对象的堆内地址
那么,在完成1后,对象的大小和地址已经确定,因此,2和3其实存在指令重排序的可能。
并且可以看到,3的操作明显比2要少,那么如果让2与3一起执行,并且反应到具体的顺序上变成了1-3-2.
先完成3,引用变量instance先指向了在堆中给对象分配的空间,然后2仍在慢慢吞吞继续。
这时候,被synchronized挡在外面的阻塞线程其实是不会有什么影响的,因为一定会等到对象创建完,首个拿锁者才会释放锁。
那么关键是在,此刻如果在3完成而2未完成这个临界点,有一个新线程调用getInstance(),那么第一个if,会怎么样?
答案是因为第一个if没在同步块里,而此时instance已经非空,指向具体内存地址了,所以直接返回此时未完成初始化的instance实例
那么如果在Singleton里有个变量int number ,有个方法int getNumber()返回number,这时候调用
Singleton.getInstance().getNumber();
会怎样?
不知道,可能会报错,或者会得到错误结果,但这就是我能想到的volatile避免的情况了。
volatile修饰变量避免指令重排序,保证1-2-3按顺序来,这样即使在首个拿锁者未释放锁前,有线程切入,当它在第一个if处得到instance非空时,此时instance的初始化也一定已经完成。
因此这就是volatile在DCL的作用了。