在Java设计模式之单汉式单例模式涉及到多线程访问时,可能用到双重检查式。代码如下:
package com.charles.singleton
public class Singleton
{
private static Singleton instance=null;
private Singleton(){}
public static Singleton getInstance()
{
if(instance==null) //point 1
{
synchronized(Singleton.class){
if(instance==null) //point 2
{
instance=new Singleton(); //May come into Exception
}
}
}
return instance;
}
}
以上代码使得当多个线程同时获取该类对象时,只能持有一个同一个对象的引用。假设同时有线程A以及线程B同时访问,当线程A进入实例之后,进入point 1,此时instance假设为null,满足条件,加锁进入point 2,此时依然满足条件,则实例化instance对象,并返回该对象实例。线程B此时访问,则不满足point 1,直接返回instance对象实例,不会第二次实例化对象。可以看出,双重检查可以使得其执行效率变高,在point 1处,并不会触发synchronized,就不会产生线程的排队等待问题,提高了效率。
其实,这种访问模式是有一定的问题存在,因为在JVM中,存在重排序的问题。在极端情况下,可能会因为重排序问题而导致线程之间的不安全。
重排序:
重排序是JVM中的一种优化代码的运行机制,其特点是语句的原子性。而上述可能引起问题的代码就在
instance=new Singleton();
这句。因为其并非原子性操作,可能会由于重排序(不会引发单线程执行结果的改变)而引起问题。在JVM中,这句话会被分成三步执行,如下:
1.JVM为其分配内存地址以及内存空间
2.使用构造方法实例化对象
3.将分配的内存地址赋予对象
JVM在执行时,如果重排序可能会有以下几种可能:
执行顺序:1 、2 、3
执行完毕,不出问题。
执行顺序:1 、 3 、2
执行完毕,产生问题。
产生问题原因,假设线程A刚执行了1,分配了地址以及空间,此时正常。执行3,将分配的地址给对象,正常。此时还未执行2,未实例化该对象,但此时另一个线程B已经到来,由于线程A已经为instance配了地址,instance不为null(但是并没有用构造方法实例化对象,即new Singleton()),返回instance。那么现在线程B拿到的单例对象就不能使用(没有实例化),而产生错误。
解决问题方法:
可以看出,产生这种错误的原因是因为JVM的重排序导致,那么我们可以使用一个关键字来禁止重排序即volatile。
private volatile static Singleton instance=null;
这样,JVM在执行时便不会对其进行重排序而产生错误。
解决问题的原理(volatile):
volatile是通过内存屏障来防止重排序问题
1。在volatile写操作前,插入StoreStore屏障
2。在volatile写操作后,插入StoreLoad屏障
3。在volatile读操作前,插入LoadLoad屏障
4。在volatile读操作后,插入LoadStore屏障