公司发的小师妹夸我好棒,因为我告诉了她项目中用的双重检查锁定是怎么回事

Posted coding元宇宙

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 semanticsintra-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类来初始化instanceJVM将推迟InstanceHolder类的初始化,直到这个类被调用才会初始化。
  • 首次执行getInstance()方法的线程将导致InstanceHolder类被初始化。
  • 由于使用了静态初始化来初始化instance,因此不需要额外的同步操作。
  • 任何一个线程第一次调用getInstance方法,都会导致InstanceHolder类的加载和初始化,同时也会会导致static修饰的instance进行初始化。

九、两种解决方案的对比

  • 1、基于类初始化的方案的实现代码更简洁。
  • 2、基于volatile的双重检查锁定的方案有一个额外的优势: 除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。
  • 3、字段延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段 的开销。
  • 4、如果需要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于volatile的延迟初始化的方案。
  • 5、如果需要对静态字段使用线程安全的延迟初始化,请使用上面介绍的基于类初始化的方案。

以上是关于公司发的小师妹夸我好棒,因为我告诉了她项目中用的双重检查锁定是怎么回事的主要内容,如果未能解决你的问题,请参考以下文章

公司发的小师妹夸我好棒,因为我告诉了她项目中用的双重检查锁定是怎么回事

公司发的小师妹夸我好棒,因为我告诉了她项目中用的双重检查锁定是怎么回事

python爬取精美壁纸,妹子夸我好棒呢|Python 主题月

公司发的小师妹问我java中的线程池,这么讲可还行?

公司发的小师妹问我java中的线程池,这么讲可还行?

公司发的小师妹问我java中的线程池,这么讲可还行?