双重检查锁定

双重检查锁定

产生原因

在写Java程序的时候,有时候会推迟高开销对象的初始化,在使用的时候在进行初始化,达到lazy loading的效果.但是进行延迟初始化的时候可能会产生很多问题(多线程环境),例如:

public class UnsafeLazyLoading {
    private static UnsafeLazyLoading unsafeLazyLoading;
    private UnsafeLazyLoading (){}
    public static UnsafeLazyLoading getInstance(){
        if( unsafeLazyLoading == null){                     //sign 1
            unsafeLazyLoading = new UnsafeLazyLoading();    //sing 2
        }
        return unsafeLazyLoading;
    }
}

就比如简单的单例模式要获取唯一的实例,如果在多线程环境下如果线程A运行到 sign 1标识的行,此时线程B运行到 sing 2标识的行,线程A就会看到需要的实例还没有被初始化,就会产生问题.

处理这个可以加同步锁,代码如下:

public class UnsafeLazyLoading {
    private static UnsafeLazyLoading unsafeLazyLoading;
    private UnsafeLazyLoading (){}
    public synchronized static UnsafeLazyLoading getInstance(){
        if( unsafeLazyLoading == null){                     //sign 1
            unsafeLazyLoading = new UnsafeLazyLoading();    //sing 2
        }
        return unsafeLazyLoading;
    }
}

但是每次在获取实例的时候都会进入同步锁,会严重影响系统的性能.所以通过双重检查锁定来实现延迟初始化,代码如下:

public class UnsafeLazyLoading {
    private static UnsafeLazyLoading unsafeLazyLoading;
    private UnsafeLazyLoading (){}
    public static UnsafeLazyLoading getInstance(){
        if( unsafeLazyLoading == null){                     //sign 1
            synchronized (UnsafeLazyLoading.class){
                if( unsafeLazyLoading == null){
                    unsafeLazyLoading = new UnsafeLazyLoading();   
                }
            }
            
        }
        return unsafeLazyLoading;
    }
}

上面这种方法看起很好

  • 在多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。
  • 在对象创建好之后,执行getInstance()将不需要获取锁,直接返回已创建好的对象。

但是执行到 sing1 标识的地方的时候,线程有可能unsafeLazyLoading不为空的时候,但是unsafeLazyloading引用的对象还有可能没有完成初始化的过程.

###问题根源

这里A2和A3虽然重排序了,但java内存模型的intra-thread semantics将确保A2一定会排在A4前面执行。因此线程A的intra-thread semantics没有改变。但A2和A3的重排序,将导致线程B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象。

在知晓了问题发生的根源之后,我们可以想出两个办法来实现线程安全的延迟初始化:

  1. 不允许2和3重排序;
  2. 允许2和3重排序,但不允许其他线程“看到”这个重排序。

以上引用内容来自 http://ifeve.com/double-checked-locking-with-delay-initialization/

解决方案

基于volatile的双重检定

只需要把要获取的实例unsafeLazyLoading声明为volatile就可以,如下:

public class UnsafeLazyLoading {
    private volatile static UnsafeLazyLoading unsafeLazyLoading;
    private UnsafeLazyLoading (){}
    public static UnsafeLazyLoading getInstance(){
        if( unsafeLazyLoading == null){                     //sign 1
            synchronized (UnsafeLazyLoading.class){
                if( unsafeLazyLoading == null){
                    unsafeLazyLoading = new UnsafeLazyLoading();   
                }
            }
            
        }
        return unsafeLazyLoading;
    }
}

当声明对象的引用为volatile后,上述说到的2和3之间的重排序,在多线程环境中将会被禁止。

基于类初始化

JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。因此就可以采用静态内部类的形式实现延迟加载的效果,像上一篇文章最后的代码一样.

package factory.pattern.singleton;
/**
 * Created by fk5431 on 6/19/17.
 */
public class SingletonStaticClass {
    //静态内部类
    private static class SingletonHodler{
        private static final SingletonStaticClass INSTANCE = new SingletonStaticClass();
    }
    private SingletonStaticClass(){}
    public static final SingletonStaticClass getInstance(){
        return SingletonHodler.INSTANCE;
    }
}

这样虽然允许了上述的2,3之间的重排序,但是非构造线程无法被重排序所影响.

#设计模式#
设计模式介绍 文章被收录于专栏

设计模式是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。其实就是经过前人反复使用总结使用得出在不同场景有对应的解决方案。

全部评论
😑
点赞 回复 分享
发布于 2023-10-09 16:33 北京
点赞 回复 分享
发布于 2023-10-09 16:56 北京

相关推荐

11-09 01:22
已编辑
东南大学 Java
高级特工穿山甲:羡慕,我秋招有家企业在茶馆组织线下面试,约我过去“喝茶详谈”😢结果我去了发现原来是人家喝茶我看着
点赞 评论 收藏
分享
像好涩一样好学:这公司我也拿过 基本明确周六加班 工资还凑活 另外下次镜头往上点儿
点赞 评论 收藏
分享
不愿透露姓名的神秘牛友
11-24 20:55
阿里国际 Java工程师 2.7k*16.0
程序员猪皮:没有超过3k的,不太好选。春招再看看
点赞 评论 收藏
分享
3 3 评论
分享
牛客网
牛客企业服务