公司发的小师妹夸我好棒,因为我告诉了她项目中用的双重检查锁定是怎么回事
Posted 三太子敖雪
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了公司发的小师妹夸我好棒,因为我告诉了她项目中用的双重检查锁定是怎么回事相关的知识,希望对你有一定的参考价值。
文章目录
前言
开心一笑
一、定义的Foo类
@Data
public class Foo {
private String name;
private int age;
public Foo() {
System.out.println("我被调用了");
}
}
二、 instance = new Foo()创建对象的步骤
memory = allocate();
// 1:分配对象的内存空间ctorInstance(memory);
// 2:初始化对象instance = memory;
// 3:设置instance
指向刚分配的内存地址
Java
语言规范规定所有线程在执行Java程序时必须要遵守intra-thread semantics
。intra-thread semantics
保证重排序不会改变单线程内的程序执行结果。- 根据重排序规则,后两条语句不存在数据依赖,因此是可以进行重排序的。
- 2和3发生了指令的重排序,但是并不影响
instance = new Foo()
初始化一个对象,因此是符合Java
语言规范的。
三、非线程安全的延迟初始化对象
public class UNsafeInit {
private static Foo instance;
public static Foo getInstance(){
if (instance == null){ // 1:线程A执行
instance = new Foo(); // 2:线程B执行
}
return instance;
}
}
3.1、可能发生两种情况
第一种情况
- 在上面的代码示例中,如果线程
A
执行到了代码1的位置。- 如果线程
B
执行到了代码2
的位置,但是这个时候线程B
可能还没有执行,正准备执行呢。- 那么线程
A
看到的是instance
还没有被实例化,就会进入这个判断再次实例化对象,两个线程都初始化了,显然不是我们想要的。
第二种情况
- 在上面的代码示例中,如果线程
A
执行到了代码1
的位置。- 线程
B
执行instance=new Instance
的操作的时候发生了指令重排序,重排序后的指令:先分配内存,然后赋值给instance
,最后再执行初始化。【赋值和初始化两个指令被重排了】。- 线程B执行到赋值给
instance
,那么此时线程A
正好执行到判断instance == null
,结果是不为null
,可能就会读取到尚未初始化完成的instance
对象。
四、使用synchronized保证线程安全的延迟初始化对象
public class SafeInit {
private static Foo instance;
public synchronized static Foo getInstance(){
if (instance == null){
instance = new Foo();
}
return instance;
}
}
- 在上面的代码示例中对
getInstance()
方法使用了synchronized
关键字来修饰。- 由于对
getInstance()
方法做了同步处理,synchronized
将导致性能开销。- 如果
getInstance()
方法被多个线程频繁的调用,将会导致程序执行性能的下降。- 如果
getInstance()
方法不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。
五、为啥要引入双重检查锁定
- 由于
synchronized
存在巨大的性能开销。因此,人们想出了一个“聪明”的技巧:双重检查锁定【Double-Checked Locking
】。人们想通过双重检查锁定来降低同步的开销。- 想法是好的,但是使用不当,可能会造成线程的安全性问题。
- 以下详细介绍了怎么产生线程的安全问题和如何解决,废话不说撸它。
六、线程不安全的双重检查锁定实现延迟初始化对象
public class DoubleCheckLockUnsafeInit {
private static Foo instance;
public static Foo getInstance(){
if (instance == null){ // 1处、第一次检查
synchronized (DoubleCheckLockUnsafeInit.class){ // 2处、加锁
if (instance == null){ // 3处、第二次检查
instance = new Foo(); // 4处、实例化对象【这里会出问题的】
}
}
}
return instance;
}
}
- 在上面的代码示例中。
- 在
1
处,如果是第一次检查instance
不为null
,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅降低synchronized
带来的性能开销。- 在
2
处,如果多个线程试图在同一时间创建对象时,这里有同步代码块,会通过加锁来保证只有一个线程能创建对象。- 在
3
处,获得锁的线程,会二次检查这个对象是否已经被初始化。- 在
4
处,对象创建好之后,执行getInstance()
方法将不需要获取锁,直接返回已创建好的对象。- 一切都是那么的美好,但是有一种情况,在线程执行到
1
处,读取到instance
不为null
时;在4
处的线程正在初始化实例instance
,但是instance
引用的对象有可能还没有完成初始化,因为发生了指令重排。4
处因为指令重排,引发的1
处拿到的实例在使用的时候发生空指针的问题。
七、基于volatile的解决方案
public class DoubleCheckLockSafeInit {
private static volatile Foo instance;
public static Foo getInstance(){
if (instance == null){
synchronized (DoubleCheckLockSafeInit.class){
if (instance == null){
instance = new Foo();
}
}
}
return instance;
}
}
- 基于上面的双重检查锁定来实现延迟初始化的方案的代码【DoubleCheckLockUnsafeInit】,把instance声明为volatile型,就可以实现线程安全的延迟初始化
- volatile 可以禁止instance = new Foo();过程中的指令重排,从而实现线程的安全。
八、基于类初始化的解决方案
8.1、提前初始化
8.1.1、提前初始化的代码实现
public class EagerInit {
private static Foo instance = new Foo();
public static Foo getInstance(){
return instance;
}
}
- 使用提前初始化可以避免每次调用
getInstance
方法时所产生的同步开销。- 好是好,但是不是我们要的延迟初始化。
8.1.2、验证类初始化时静态实例被初始化
public class EagerInitTest {
public static void main(String[] args) {
EagerInit eagerInit = new EagerInit();
}
}
- 静态变量属于类,只执行一次。
JVM
在类的初始化阶段【即在Class
被加载后,且被线程使用之前】,会执行类的初始化。- 在执行类的初始化期间,
JVM
会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。在静态初始化期间,内存的写入操作将自动对所有的线程可见。- 在创建
EagerInit
对象的时候,控制台打印了Foo
类中无参构造中的输出。- 证实了类的初始化会初始化类中定义的静态字段,而且
instance
属于类只会初始化一次。
8.2、基于提前初始化改造的类的延迟初始化
public class SafeClassInit {
private static class InstanceHolder{
public static Foo instance = new Foo();
}
public static Foo getInstance(){
return InstanceHolder.instance;
}
}
- 这里是用了一个静态InstanceHolder类来初始化
instance
。JVM
将推迟InstanceHolder
类的初始化,直到这个类被调用才会初始化。- 首次执行
getInstance()
方法的线程将导致InstanceHolder
类被初始化。- 由于使用了静态初始化来初始化
instance
,因此不需要额外的同步操作。- 任何一个线程第一次调用
getInstance
方法,都会导致Instance
Holder类的加载和初始化,同时也会会导致static
修饰的instance
进行初始化。
九、两种解决方案的对比
- 1、基于类初始化的方案的实现代码更简洁。
- 2、基于
volatile
的双重检查锁定的方案有一个额外的优势: 除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。- 3、字段延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段 的开销。
- 4、如果需要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于
volatile
的延迟初始化的方案。- 5、如果需要对静态字段使用线程安全的延迟初始化,请使用上面介绍的基于类初始化的方案。
以上是关于公司发的小师妹夸我好棒,因为我告诉了她项目中用的双重检查锁定是怎么回事的主要内容,如果未能解决你的问题,请参考以下文章
公司发的小师妹夸我好棒,因为我告诉了她项目中用的双重检查锁定是怎么回事
公司发的小师妹夸我好棒,因为我告诉了她项目中用的双重检查锁定是怎么回事