Java中的双重检查锁定
双重检查锁定又称双重效验锁,以前常常用于Java中的单例模式,在并发编程中的线程池中常常用到该模式,并且在Spring中DI(依赖注入)也用到该模式的思想,当Spring运行的时候将我们加入注解的bean(Java对象)遍历出来,并创建其相关的一个实例,在程序的运行中,如果遇到要操作该对象的时候,便使用Spring为我们创建的该类的单例进行相关的操作。但是如何确保只生成一个单例呢?我在之前写过一篇博客,详细的讲述了单例模式的相关概念:
https://blog.csdn.net/weixin_42504145/article/details/85006406 后端---Java设计模式之单例模式详解
有需要了解的朋友可以看看,在今天的这篇博客中我们只是单独讲解一下双重效验锁的一些知识和问题。
双重检查锁定(DCL Double Checked Locking)的由来
在java程序中,有时候可能需要推延一些高开销的对象进行初始化的操作,并且只有在使用这些对象的时候进行初始化。此时,程序员可能会采用延迟初始化。但要正确的实现线程安全的延迟初始化需要一些技巧,否则会出现一些问题,比如我们看下面这段代码:
SCL代码
public class UnsafeLazyInitialization{
private static Instance instance;
public static Instance getInstance(){
if(instance==null) //1:A线程执行
instance=new Instance; //2:B线程执行
return instance;
}
}
我们可以看出在这段代码中,在UnsafeLazyInitialization中,假设A线程执行代码1的同时,Bxiancheng只想代码2.此时,线程A可能会看到instance引用的对象还没有完成初始化(出现这种情况的原因是编译器和处理器对我们的代码进行了重排序)
所以对这个类的getInstance()方法我们进行了一些优化,加锁的代码如下:
public class UnsafeLazyInitialization{
private static Instance instance;
public synchronized static Instance getInstance(){
if(instance==null)
instance=new Instance;
return instance;
}
}
我们对getInstance()的方法进行了加锁处理,但是synchroized关键字导致了性能下降,如果这个方法被多个线程进行调用,将会导致程序的执行性能下降。所以就有了下面的DCL双重效验锁定
public class DoubleCheckedLocking{ //1
private static Instance instance; //2
public static Instance getInstance(){ //3
if(instance==null) { //4 第一次检查
synchronized{DoubleCheckedLocking.class}{ //5 加锁
if(instance == null) //6 第二次检查
instance = new Instance(); //7 问题的根源所在
} //8
} //9
return instance; //10
} //11
}
通过上面的代码我们可以看到,假如有多个方法进行调用,假如说第一次检查不为null,即已经生成了一个单例,那么就不要执行加锁的代码,直接返回结果,可以大幅度的降低synchroized带来的性能开销,但是一个隐患出现了,当代码执行到第4行的时候 代码读取到的Instance不为null时,Instance引用的对象有可能还没有完成初始化。
why???
这就要涉及到计算机底层的一些知识了,当我们编写出一段代码的时候,代码会被计算机执行,生成我们想要的结果,代码之间有着顺序的逻辑关系,但是在Cpu执行的时候会将我们的代码进行重排序,就是计算机并不一定保证代码的执行顺序与你书写代码的顺序一致,在保证结果一致的情况下,Cpu会把代码执行的顺序进行改变,从而达到性能的最大化。
我们了解到这份知识后,再来看刚给出的代码第7行(instance=new instance();创建了一个对象。这段代码又可以分解为下面3行伪代码:
memory=allocate(); // 1:分配对象的内存空间
ctroInstance(memory); // 2:初始化对象
instance=memory; // 3:设置Instance指向刚分配内存地址
所以当进行了重排序的时候,2和3的位置有可能互换,这样会导致我们实例指针执行了一段空间不为null,但是并没有真正完成初始化对象这个操作。
解决方案
1.给instance声明成volatile (volatile关键字的语义会禁止一个共享变量的重排序)
2.该用静态内部类来解决