错误的双重检查锁

错误的双重检查锁

在Java多线程的程序中,为了降低初始化类和创建对象的开销,经常会使用延迟初始化的方式,延迟初始化就会用到双重检查锁来实现,但很多双重检查锁的用法是错误的,这里介绍一下错误的原因和怎么实现正确的双重检查锁。

双重检查锁的由来

延迟初始化同步问题

public class UnSafeLazyInit {

    private static Instance instance;

    public static Instance getInstance(){

        if (Objects.isNull(instance)){   //1.A线程执行
            instance = new Instance();    //2.B线程支持
        }
        return instance;
    }
}

从图中可以看到,如果线程A执行到1,线程B执行到2 线程A可能看到instance还没有初始化,这种情况会重复初始化

使用synchronized优化

因此我们考虑使用加锁来处理多线程问题做同步处理

public class UnSafeLazyInit {

    private static Instance instance;

    public synchronized static Instance getInstance(){

        if (Objects.isNull(instance)){   //1.A线程执行
            instance = new Instance();    //2.B线程支持
        }
        return instance;
    }
}

但是如果getInstance()方法被多线程频繁调用,synchronized锁带来的性能开销将会很大,如果不被频繁调用,这个结果还是可以接受的

使用双重检查锁优化

public class UnSafeLazyInit {

    private static Instance instance;

    public  static Instance getInstance(){

        if (Objects.isNull(instance)){           //第一次检查
            synchronized (UnSafeLazyInit.class){ //加锁
                if (Objects.isNull(instance)){   //第二次检查
                    instance = new Instance();    //问题出现的位置
                }
            }
        }
       return instance;
    }
}

这里在方法内部先进行一次是否为空判断,这一步骤可以降低在instance已经初始化完毕的情况,还进行加锁操作的性能开销(synchronized方法的缺点),只有确定对象为空,才去竞争锁,去做对象的初始化,这种解决方案看起来已经很完美了

错误的原因

instance = new Instance(); 

这段代码在多线程执行的时候,会出现问题,原因是因为,如果线程A执行到第一次检查的时候,可能判断instance已经不为空了,但是这个时候instance还没初始化完毕,原因就在于这段代码可以分为三步

memory = allocate();  //分配内存空间
initObject(); //初始化对象
instance = memory;  //将instance指向分配的内存空间地址

这段代码可能发生重排序,重排序之后的顺序如下

memory = allocate(); //分配内存空间
instance = memory; //将instance指向分配的内存空间地址
initObject(); //初始化对象

Java语言规范中表明:允许在单线程中,不会改变单线程执行结果的重排序
在单线程中,上面的重排序并不会影响执行结果,所以这部分在单线程是没有问题的,但是Java语言规范并没有说多线程也适用。看下图更能清楚的理解

image.png

image.png

基于volatile的解决方案

volatile的作用主要用两个:
1、保证volatile修饰的变量对所有线程的可见性。
2、禁止指令重排序优化。

public class SafeLazyInit {

    private static volatile Instance instance;

    public  static Instance getInstance(){

        if (Objects.isNull(instance)){           //第一次检查
            synchronized (SafeLazyInit.class){ //加锁
                if (Objects.isNull(instance)){   //第二次检查
                    instance = new Instance();    //禁止指令重排序
                }
            }
        }
       return instance;
    }
}

将instance声明为volatile之后,问题出现的位置的三步执行就不能再重排序,这样就保证了多线程情况下的问题


image.png

基于类初始化的解决方案

JVM会在类初始化阶段(Class被加载后,且被线程使用前),会执行初始化,JVM会首先获取锁,这个锁可以同步多线程对同一个类的初始化,基于这种特点,我们可以使用另一种解决方案

public class InstanceFactory{

    private static class InstanceHolder{
        public static Instance instance = new Instance();
    }
    public static Instance getInstance(){
        return InstanceHolder.instance;
    }
}

初始化流程
1.线程A执行初始化,获取锁成功,判断是否已经初始化,如果已经初始化就释放锁,访问对象
2.如果线程A执行初始化,发现对象还未初始化,就执行初始化流程,并将初始化对象设置为初始化中状态,这时线程B也要执行,发现对象已经在初始化中,就会循环等待获取锁
3.A线程初始化完毕,B线程获取锁,发现已经初始化完毕,就立即释放锁,退出初始化

image.png

孰优孰略

对比两种方式我们发现,基于类的初始化更加简洁简单,但是基于volatile的方案,更加灵活,它不仅可以对静态字段实现延迟初始化,也可以对普通字段实现延迟初始化

写到最后

基于延迟的初始化降低了类初始化和创建实例的开销,但是也增加了访问延迟初始化字段的开销。大多数时候,正常初始化要优于延迟初始化,但是如果确实需要延迟初始化,请参考上面两种模式,选择其一。

参考书籍:Java并发编程的艺术

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容