Java对象延迟初始化的实现

一、什么是延迟初始化?

在Java多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销。
延迟初始化实际上就是:当我们要进行一些高开销的对象初始化操作时,只有在使用这些对象时才进行初始化。最显著的意义在于,假如程序实际上不会用到这些类,那初始化它们的开销就会被完全避免。

二、延迟初始化的错误实现方式

1、线程不安全的延迟初始化

public class UnsafeLazyInitialization {
    private static Instance instance;

    public static Instance getInstance() {
        if (instance == null) //1:A线程执行
            instance = new Instance(); //2:B线程执行
        return instance;
    }

    static class Instance {
    }
}

在UnsafeLazyInitialization类中,假设线程A执行代码1的同时,B线程执行代码2。此时线程A很可能看到instance引用的对象还没有完成初始化。所以我们必须对1、2这两步操作进行同步处理。

2、直接使用synchronized进行同步-有巨大的性能开销

public class SafeLazyInitialization {
    private static Instance instance;

    public synchronized static Instance getInstance() {
        if (instance == null)
            instance = new Instance();
        return instance;
    }

    static class Instance {
    }
}

在早期的JVM中,使用synchronized存在巨大的性能开销,这在实际的应用中时几乎不可能被接受的。

3、双重检查锁定-看似聪明的解决方案

public class DoubleCheckedLocking { //1
    private static Instance instance; //2

    public static Instance getInstance() { //3
        if (instance == null) { //4:第一次检查
            synchronized (DoubleCheckedLocking.class) { //5:加锁
                if (instance == null) //6:第二次检查
                    instance = new Instance(); //7:问题的根源出在这里
            } //8
        } //9
        return instance; //10
    } //11
    static class Instance {
    }
}

为了克服同步带来的大量开销,人们想得到了双重检查锁定这一看似聪明的解决方案。目的是仅仅对一开始竞争状态的getInstance加锁,带来开销。

但由于代码可能的重排序,直接使用上述代码是一种错误的优化。原因如下:
示例代码第七行,即 instance = new Instance(); 可以分解为如下的三行伪代码

memory = allocate();//1:分配对象的内存空间
ctorInstance(memory);//2:初始化对象
instance = memory;//3:设置instance指向刚分配的内存地址

假如2、3之间发生重排序,可能顺序变成如下这样

memory = allocate();//1:分配对象的内存空间
instance = memory;//3:设置instance指向刚分配的内存地址
ctorInstance(memory);//2:初始化对象

也就是说,当instance已经指向分配的内存地址时,对象还没有被初始化。
我们再回到示例代码

public class DoubleCheckedLocking { //1
    private static Instance instance; //2

    public static Instance getInstance() { //3
        if (instance == null) { //4:第一次检查
            synchronized (DoubleCheckedLocking.class) { //5:加锁
                if (instance == null) //6:第二次检查
                    instance = new Instance(); //7:问题的根源出在这里
            } //8
        } //9
        return instance; //10
    } //11
    static class Instance {
    }
}

当线程A在进行代码的第7行,即new Instance时,内部发生了重排序,即 instance = memory 在 ctorInstance(memory) 后进行。假如进程A刚刚进行到这两步之间。而进程B恰巧在第四行(第一次检查)处进行判断。那么线程B判断instance不为null,很可能向下进行,进而访问instance所引用的对象。而这时进程A尚未初始化instance!从而程序发生错误。

三、延迟初始化的正确实现方式

在上面的说明中了解了问题的根源后,我们可以很容易想到两个方法来实现线程安全的延迟初始化。
(1)不允许伪代码中的2和3两行重排序
(2)允许2和3重排序,但不允许其他线程"看到"这个重排序

1、基于volatile的解决方案

我们只要对之前双重检查锁定的代码进行一些小小的修改,就可以实现我们期望中的延迟初始化。

public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;

    public static Instance getInstance() {
        if (instance == null) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (instance == null)
                    instance = new Instance();//instance为volatile
            }
        }
        return instance;
    }

    static class Instance {
    }
}

当instance的引用被声明为volatile时,创建对象时的重排序就将在多线程环境中被禁止。从而实现了用双重检查锁定来实现延迟初始化。

注:这个方案需要JDK5以上版本,因为自JDK5开始使用新的JSR-133内存模型,这个规范增强了volatile的语义。

2、基于类初始化锁的解决方案

JVM在类的初始化阶段(即Class被加载后,被线程使用前),会执行类的初始化。在执行类的初始化期间,JVM会获取一个锁,这个锁可以同步多个线程对一个类初始化。基于这个特性,我们可以用以下的方式来实现延迟初始化。
注意这个锁是对于类的初始化,而非对象的!

public class InstanceFactory {
    private static class InstanceHolder {//利用这个类的初始化锁
        public static Instance instance = new Instance();
    }

    public static Instance getInstance() {
        return InstanceHolder.instance; //这里将InstanceHolder被初始化
    }
    static class Instance {
    }
}

当getInstance第一次被调用,发生竞争时,InstanceHolder将被初始化。其中的静态变量instance也在此时被初始化。而InstanceHolder这个类的初始化锁保证了instance的初始化是被同步的。即无论new instance时是否发生重排序,都不会被其他线程所看到。从而解决了同步问题。

类的初始化锁相关知识在此不赘述,可参考《The Art of Java Concurrency Programming》相关篇目。

四、两种解决方案的对比

我们很容易可以发现,基于类初始化锁的方案的实现代码要更加简洁。但基于volatile的双重检查锁定方案有一个额外的优势,它可以对实例字段实现延迟初始化。
当我们进行延迟初始化处理时,面对实例字段我们使用基于volatile的方案,面对静态字段我们使用集于类初始化锁的方案。

2021-1-16

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容