双检锁/双重校验锁 双层对空判断困扰了很久。实例
public class Singleton {
private volatile static Singleton singleton;
//私有构造函数避免调用
private Singleton (){}
public static Singleton getSingleton() {
// 先判断对象是否创建过
if (singleton == null) {
//类对象加锁
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
//字节码层
//JIT CPU 可能对如下指令进行重排序
// 1.分配空间
// 2.初始化
// 3.引用赋值
如果指令重排后指令如下:
//1.分配空间
//3.引用赋值 如果在当前指令执行完后,有其他线程获取到实例,将拿到未初始化的实例;
//2. 初始化
}
}
}
return singleton;
}
}
解释:当A与B同时调用getSingleton时,判断第一个if都为空,这时A拿到锁,进行第二层if判断,条件成立new了一个对象;
B在外层等待,A创建完成,释放锁,B拿到锁,进行第二层if判断,条件不成立,结束释放锁。C调用getSingleton时第一层判断不成立,直接拿到singleton对象返回,避免进入锁,减少性能开销。
进一步理解:其中两次判空,第一次判空是,减少多线程情况下,进入同步代码块的次数,第二次判空,是防止多线程(A,B两种线程的情况下A,B 同时调用了 getSingleton 方法,都同时,进入到第一层if(singleton==null){} 内,竞态条件下,如果A 拿到了对象锁,进入到同步代码块,B阻塞等待,等待创建了实例对象,释放了锁后,B进入同步代码块,但是此时第二层if(singleton==null){} 判断,singleton 不为空,直接返回singleton);
总结:其中有两次判断是否为空的语句,第一次是为了提高效率,避免每次都要执行同步代码块,第二次判空,是为了避免多线程带来的不安全,当两个线程同时对第一个判断为空时,均会先后进入同步代码块,此时,若没有第二个判空条件,则会引来创建多个实例。
volite 关键字:
采用volatile关键字修饰很有必要
这句代码事实上是分三步:
singleton = new Singleton();
- 为singleton 分配内存空间
- 初始化singleton
- 将singleton 指向分配的内存地址
但是,jvm会有指令重排的特性,执行顺序有可能改变,不是按照123的顺序,可能是132,这样就会导致一个线程获得没有初始化的实例
如:t1执行了13,此时t2调用了getInstance()后发现singleton 已经不为空了,因此返回singleton ,但是这时singleton 还没有被初始化
volatile就可以禁止jvm的指令重排,保证在多线程环境下也能正常运行
想进一步了解:推荐
深入理解Java并发之synchronized实现原理
https://blog.csdn.net/javazejian/article/details/72828483
参考: