延迟加载的一些知识和误区

栏目: Java · 发布时间: 6年前

内容简介:原文地址最近开始看《java并发编程的艺术》一书,从里面get到了好些知识上的盲点,下面就延迟加载这个问题来分析一波~~从上面的代码片段里,很容易发现在多线程并发情况下去调用getInstance是会出问题的.当A线程和B线程同时进入到步骤1处,便会实例化两个对象出来,A和B访问到的对象就不会是同一个。

原文地址 www.hcyhj.cn/2018/11/21/…

最近开始看《java并发编程的艺术》一书,从里面get到了好些知识上的盲点,下面就延迟加载这个问题来分析一波~~

首先咱们来看一段简单的代码:

public class DelayLoad {

    private DelayLoad() {
    }

    private static DelayLoad instance;

    public static DelayLoad getInstance() {
        if (instance == null) {               //步骤1
            instance = new DelayLoad();       //步骤2
        }
        return instance;
    }
}
复制代码

从上面的代码片段里,很容易发现在多线程并发情况下去调用getInstance是会出问题的.当A线程和B线程同时进入到步骤1处,便会实例化两个对象出来,A和B访问到的对象就不会是同一个。

下面升级一下,加上同步关键字synchronized

public class DelayLoad {

    private DelayLoad() {
    }

    private static DelayLoad instance;

    public static synchronized DelayLoad getInstance() {
        if (instance == null) {
            instance = new DelayLoad();
        }
        return instance;
    }
}
复制代码

代码改成这样后,可以完全保证并发情况下获取的instance实例都会是同一个,但是多个线程同时调用synchronized 修饰的方法,会有获取锁以及释放锁操作,这里会造成大量的性能损耗,得不偿失!

继续改造一下,看能不能提升下性能:

public class DelayLoad {

    private DelayLoad() {
    }

    private static DelayLoad instance;

    public static  DelayLoad getInstance() {
        if (instance == null) {                     //第一次检查
            synchronized (DelayLoad.class){
                if (instance == null) {             //第二次检查
                    instance = new DelayLoad();    //创建实例
                }
            }
        }
        return instance;
    }
}
复制代码

咱们这里用双重检测的方法来实现这个单例懒加载,用这种策略看上去貌似没有什么问题,多线程并发的情况下往往也就是在第一次检查时都会直接返回实例,这样就不会造成性能损耗. 但是 ,这里有可能出现instance不一致的问题。对于这个问题我们得先了解 对象的初始化过程

对象的初始化过程

1.在堆上为DelayLoad对象分配足够大的空间,所有属性和方法都被设置成缺省值(数字为0,字符为null,布尔为false,而所有引用被设置成null)。 2.执行构造函数检查是否有父类,如果有父类会先调用父类的构造函数,这里假设DelayLoad没有父类,执行缺省值字段的赋值即方法的初始化动作。 3.执行构造函数.

上面创建实例的那一步在cpu上可能经过如下操作:

memory = allocate(); //1分配对象内存空间
initInstance(memory);//2初始化对象
instance = memory;  //3设置instance指向更分配的内存地址
复制代码

但实际上执行的过程中,2和3步骤有可能进行指令重排,也就是按132的顺序执行,这样就会导致instance指向的是一个属性和值都是缺省值的对象。然后被一个竞争线程所拿到并进行使用。

延迟加载的一些知识和误区

目前有两种解决办法

第一种:给实例变量加上volatile 关键字修饰

public class DelayLoad {

    private DelayLoad() {
    }

    private static volatile DelayLoad instance;

    public static  DelayLoad getInstance() {
        if (instance == null) {
            synchronized (DelayLoad.class){
                if (instance == null) {
                    instance = new DelayLoad();
                }
            }
        }
        return instance;
    }
}

复制代码

代码改成上述情况后,在设置instance指向更分配的内存地址之前会有StoreStore内存屏障,执行代码会禁止指令重排,这样咱们拿到的instance都是经过初始化过的。

第二种:基于类初始化的解决方案

public class DelayLoad {

    private DelayLoad() {
    }

    private static class DelayLoadHolder {
        public static DelayLoad instance = new DelayLoad();
    }

    public static DelayLoad getInstance() {
        return DelayLoadHolder .instance;
    }
}
复制代码

该延迟加载方案是基于JVM的类初始化原理实现的。在执行类的初始化期间,JVM会去获取一个锁,该锁可以同步多个线程对同一个类的初始化。类只会被加载一次,在加载完成之前对其他线程都是不可见的。这样也能保证获取到的instance也是同一个。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

网飞传奇

网飞传奇

[美]吉娜·基廷 / 谭永乐 / 中信出版社 / 2014-1-1 / 42

飞的历史充满了传奇色彩,它的崛起伴随着复杂斗争、幸运转折、个人背叛……它自身的历史比它出租的那些电影还要更富有戏剧性。网飞在1997年建立,而建立的原因仅仅是因为创始人伦道夫和哈斯廷斯没有按时归还租借的DVD,还要缴纳因此而产生的滞纳金。 1999年,网飞公司摒弃了百视达的“每片付租”模式,转而采用了一种订阅模式:用户只需要支付固定费用,就能尽情租片观赏,免去了到期还片日、滞纳金、运费和手续......一起来看看 《网飞传奇》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具