Skip to content

四火的唠叨

一个纯正程序员的啰嗦

Menu
  • 所有文章
  • About Me
  • 关于四火
  • 旅行映像
  • 独立游戏
  • 资源链接
Menu

从 DCL 的对象安全发布谈起

Posted on 12/28/201306/23/2019 by 四火

lock

对于 DCL(Double Check Lock)情况下的对象安全发布,一直理解得不足够清楚;在通过和同事,以及和互联网上一些朋友的讨论之后,我觉得已经把问题搞清楚了。我把我对这个问题的理解简要记录在这里。

现在有代码 A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class T {
    private static volatile T instance;
    public M m; // 这里没有 final 修饰
 
    public static T getInstance() {
        if (null == instance) {
            synchronized (T.class) {
                if (null == instance) {
                    instance = new T();
                    instance.m = new M();
                }
            }
        }
        return instance;
    }
}

以及代码 B: 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class T {
    private static volatile T instance;
    public M m; // 这里没有 final 修饰
 
    public static T getInstance() {
        if (null == instance) {
            synchronized (T.class) {
                if (null == instance) {
                    T temp = new T();
                    temp.m = new M();
                    instance = temp;
                }
            }
        }
        return instance;
    }
}

这两段代码是否做到了对象安全发布?

这里需要稍微解释一下,所谓对象安全发布,在这里可以这样理解,有一个线程 X 调用 getInstance 方法,第一次来获取对象,instance 为空,这个时候进入同步块,初始化了 instance 并返回;这以后另一个线程 Y 也调用 getInstance 方法,不进入同步块了,获取到的 instance 对象是否一定是预期的—— 即对象的 m 属性不为空?如果是,表示对象被安全发布了,反之则不是。

happens-before 一致性

仔细读了读 JSR-133 的规范文档,里面定义了 happens-before(hb)一致性:

Happens-before consistency says that a read r of a variable v is allowed to observe a write w to v if, in the happens-before partial order of the execution trace:

  • r is not ordered before w (i.e., it is not the case that r hb w), and 
  • there is no intervening write w' to v (i.e., no write w' to v such that w hb w' hb r).

这就是说,如果任何时候在满足以下这样两个条件的情况下,对一个对象的读操作 r,都能得到对于对象的写操作 w 的结果(读的时候要能返回写的结果),我们就认为它就是满足 happens-before 一致性的:

  • 读必须不能发生在写操作之前;
  • 没有一个中间的写操作 w' 发生在 w 和 r 之间。

满足这样一致性的内存模型,是一种极度简化的内存模型,它允许 JVM 实现的时候,对于绝大多数情况下不需要满足 happens-before 的对象和操作,可以在保证单个线程运行结果正确的情况下做尽可能多的优化,比如代码乱序执行,比如从主内存中缓存某些变量到寄存器等等。

volatile 和 happends-before 的关系

A write to a volatile field happens-before every subsequent read of that volatile.

就是说,对于 volatile 修饰的属性的读写操作满足 happens-before 一致性。

再结合代码来看,代码 A 对于 m 的赋值发生在 volatile 修饰的 instance 之后,不能保证线程 X 中给 instance 的属性赋的值 new M() 能被线程 Y 看到;而代码 B 所有对于实例初始化的操作都放 instance=temp;(即对 volatile 修饰的属性 instance 的写操作)之前,这些操作的结果都是“ 可见的”。也就是说,代码 A 无法安全发布对象,但是代码 B 可以。

需要说明的是,如果对于代码 B,干脆去掉属性 m,但是也拿掉 volatile,变成如下情况呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class T {
    private static T instance;
 
    public static T getInstance() {
        if (null == instance) {
            synchronized (T.class) {
                if (null == instance) {
                    instance = new T();
                }
            }
        }
        return instance;
    }
}

这种情况下对象又无法安全发布了,因为没有了 volatile 的约束,对象初始化的行为和把对象赋给 instance 的行为是乱序的(前面已经介绍过了,只需要保证结果正确即可,在这里就是保证 getInstance 方法返回的结果是正确的;但是,在 getInstance 方法内部,当 instance 不为空的时候,T 的初始化行为却未必已经完成),这个就有可能取到一个没有初始化完成的残缺的对象。

除了 volatile 关键字以外,还有哪些情况下也满足 happens-before 一致性呢?

  • Each action in a thread happens-before every subsequent action in that thread.
  • An unlock on a monitor happens-before every subsequent lock on that monitor.
  • A write to a volatile field happens-before every subsequent read of that volatile.
  • A call to start() on a thread happens-before any actions in the started thread.
  • All actions in a thread happen-before any other thread successfully returns from a join() on that thread.
  • If an action a happens-before an action b, and b happens before an action c, then a happens- before c.

简单说,就是同一个线程的后续行为,加锁,启动子线程,线程 join() 操作和满足传递性的三个操作这六种情况,其他所有的情况都不具备 happens-before 一致性。值得一提的是其中的第一条,需要理解其中的“subsequent action”(后续行为),比如调用一个方法返回的结果应当是正确的,类的每一条静态语句的执行结果也是正确的。这是 hb 内存模型在降低约束、提供更多优化可能的同时,必须要做到的正确性上的保证。

final 在 JSR-133 中的增强

由于 final 的值本身是不可被重写入的(所谓的“ 不变” 对象),那么编译器就可以针对这一点进行优化:

Compilers are allowed to keep the value of a final field cached in a register and not reload it from memory in situations where a non-final field would have to be reloaded.

编译器可以把 final 修饰的属性的值缓存在寄存器里面,并且在执行过程中不重新加载它。

但是,如果对象属性不使用 final 修饰,在构造器调用完毕之后,其他线程未必能看到在构造器中给对象实例属性赋的真实值(除非有其他可行的方式保证 happens-before 一致性,比如前面提到的代码 B):

A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object’s final fields.

仅当在使用 final 修饰属性的情况下,才可以保证在对象初始化完成之后,外部能够看到对象正确的属性值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class FinalFieldExample {
    final int x;
    int y;
    static FinalFieldExample f;
 
    public FinalFieldExample() {
        x = 3;
        y = 4;
    }
 
    static void writer() {
        f = new FinalFieldExample();
    }
 
    static void reader() {
        if (f != null) {
            int i = f.x; // guaranteed to see 3
            int j = f.y; // could see 0
        }
    }
}

这个例子正式规范里面给出的,上面属性 x 使用了 final 修饰,而 y 没有,在某一时刻,一个线程调用 writer() 的时候,FinalFieldExample 被初始化;之后另一个线程去取得这个对象,首先最开始的时候 f 未必一定不为空,因为 f 并没有任何 happens-before 一致性保证(f 可能被赋了一个构造并未完成的对象),其次对于属性 x,由于 final 关键字的效应,f 不为空的时候,f 已经初始化完成,所以 f.x 一定为准确的 3,但是 f.y 就不一定了。

还有其它的单例对象安全发布的方式:

1
2
3
4
5
6
7
8
public class T {
  private static final T instance = new T(); // final 可少吗?
  public final M m = new M(); // final 可少吗?
 
  public static T getInstance() {
    return instance;
  }
}

这种是很常见的,还有一种叫做 Initialization On Demand Holder 的方式:

1
2
3
4
5
6
7
8
9
10
11
class T {
    public final M m = new M(); // final 可少吗?
 
    private static class LazyHolder {
        public static T instance = new T();
    }
 
    public static T getInstance() {
        return LazyHolder.instance;
    }
}

这两段代码在不使用的时候都可以保证对象安全发布的,因为这种写法下,对于属性的初始化会在对象的构造器调用前完成,这是前面说的 happens-before 的第一种(Each action in a thread happens-before every subsequent action in that thread.),属于对程序正确性的要求。

附件:JSR-133 规范下载

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

×Scan to share with WeChat

你可能也喜欢看:

  1. 再议单例模式和静态类
  2. Java 多线程发展简史
  3. java.util.concurrent 并发包诸类概览
  4. 泛型传递
  5. 运行时动态增加枚举类型

8 thoughts on “从 DCL 的对象安全发布谈起”

  1. luchy0120 says:
    01/09/2015 at 11:06 AM

    <code>class T {
        private static T instance;
     
        public static T getInstance() {
            if (null == instance) {
                synchronized (T.class) {
                    if (null == instance) {
                        instance = new T();
                    }
                }
            }
            return instance;
        }
    }</code>
    这种写法错误的原因是什么呢?synchronized 的语义保证了在退出语句块时本地内存刷新
    主内存。假设 A 线程内 new T() 是乱序的,但是 B 线程读取 instance 变量是主内存中的,应该还是 null,因为 A 线程还没退出 synchronize 块,还没来得及回写主内存啊。

    Reply
    1. 四火 says:
      01/10/2015 at 12:12 PM

      因为 synchronized 关键字,线程 B 检查发现 instance 确实不为空,所以就不进入同步块,这没有问题,然而,线程 B 得到的 instance 却未必是一个已经完全实例化了的 T 对象,换言之,并没有任何途径保证“new T()”happens-before“ 给 instance 的赋值”。

      如果你还是无法理解,可以看看这篇文章 http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html 帮助你理解

      Reply
      1. luchy0120 says:
        01/12/2015 at 5:18 PM

        这个问题我理解了,是因为 A 线程的解锁操作和 B 线程对 instance 的第一次检查(同
        步块之前的)之间没有 happens-before 关系,线程 A 虽然执行了刷回主内存的操作,
        但是线程 B 无法保证能读到正确的。因为脱离了监视器之间的 happens-before 约束。

        Reply
  2. luchy0120 says:
    01/09/2015 at 10:59 AM

    class T {
        private static volatile T instance;
     
        public static T getInstance() {
            if (null == instance) {
                synchronized (T.class) {
                    if (null == instance) {
                        instance = new T();
                    }
                }
            }
            return instance;
        }
    }
     
     
    如果是以上写法,是安全发布么?
    instance = new T();
    这一句是不是相当于
    T temp = new T();
    instance = temp;
    呢?
    instance = new T(); 不是一个原子写操作,会有问题么?

    Reply
    1. 四火 says:
      01/10/2015 at 12:02 PM

      是安全发布。就是因为 volatile 保证了“new T()” 必定 happens-before 于“ 对 instance 的赋值”。

      T temp=new T(); instance=temp; 这一小段确实不是原子操作,但是,因为 instance 有 volatile 修饰,那么无论赋值前怎么个执行顺序,都一定 happens-before 于给 instance 赋值这一步之前,因此是没有问题的。

      Reply
      1. luchy0120 says:
        01/12/2015 at 3:50 PM

        为什么 volatile 能保证 new T() 必定 happens-before 于“ 对象 instance 的赋值”?volatile 只是保证每次的读操作都能读到最后写的内容。写成 T temp = new T();instance = temp; 是因为可以利用 Each action in a thread happens-before every subsequent action in that thread。再利用 valotile 的写 happens-before 读。可以参考 http://www.infoq.com/cn/articles/java-memory-model-4。
         
         
         
         

        Reply
  3. engineer says:
    12/31/2013 at 11:17 PM

    final 怎么从 jmm 角度理解?没明白 final 是怎么保证可见性的。如果 final 的是一个对象呢?final 保证引用不变,但如何保证属性值发生变化时的可见性呢?

    Reply
    1. 四火 says:
      01/01/2014 at 9:03 AM

      请阅读规范,这是我的理解:

      final 假定你的对象是事实不可变的,并且保证在构造方法返回的时候,取得的是正确构造完成的对象(也包括这个对象内部的引用对外真实可见);但是这以后的时间通过调用 final 修饰的对象的方法,引起 final 对象内部的改变,规范里面并没有说明(我没有找到),因此是无法保证的。

      when the object is seen by another thread, that thread will always see the correctly constructed version of that object’s final fields. It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are.

      Reply

Leave a Reply to luchy0120 Cancel reply

Your email address will not be published. Required fields are marked *

订阅·联系

四火,啰嗦的程序员一枚,现居西雅图

Amazon Google Groovy Hadoop Haskell Java JavaScript LeetCode Oracle Python Spark 互联网 前端 华为 历史 同步 团队 图解笔记 基础设施 工作 工作流 工具 工程师 应用系统 异步 微博 思考 技术 数据库 曼联 测试 生活 程序员 管理 系统设计 缓存 编码 编程范型 英语 西雅图 设计 评审 问题 面试 项目

分类

  • Algorithm and Data Structure (30)
  • Concurrency and Asynchronization (6)
  • System Architecture and Design (43)
  • Distributed System (18)
  • Tools Frameworks and Libs (13)
  • Storage and Data Access (8)
  • Front-end Development (33)
  • Programming Languages and Paradigms (55)
  • Testing and Quality Assurance (4)
  • Network and Communication (6)
  • Authentication and Authorization (6)
  • Automation and Operation Excellence (13)
  • Big Data and Machine Learning (5)
  • Product Design (7)
  • Hiring and Interviews (14)
  • Project and Team Management (14)
  • Engineering Culture (17)
  • Critical Thinking (25)
  • Career Growth (57)
  • Life Experience and Thoughts (45)

推荐文章

  • 谈谈分布式锁
  • 常见分布式系统设计图解(汇总)
  • 系统设计中的快速估算技巧
  • 从链表存在环的问题说起
  • 技术面试中,什么样的问题才是好问题?
  • 从物理时钟到逻辑时钟
  • 近期面试观摩的一些思考
  • RSA 背后的算法
  • 谈谈 Ops(汇总 + 最终篇):工具和实践
  • 不要让业务牵着鼻子走
  • 倔强的程序员
  • 谈谈微信的信息流
  • 评审的艺术——谈谈现实中的代码评审
  • Blog 安全问题小记
  • 求第 K 个数的问题
  • 一些前端框架的比较(下)——Ember.js 和 React
  • 一些前端框架的比较(上)——GWT、AngularJS 和 Backbone.js
  • 工作流系统的设计
  • Spark 的性能调优
  • “残酷” 的事实
  • 七年工作,几个故事
  • 从 Java 和 JavaScript 来学习 Haskell 和 Groovy(汇总)
  • 一道随机数题目的求解
  • 层次
  • Dynamo 的实现技术和去中心化
  • 也谈谈全栈工程师
  • 多重继承的演变
  • 编程范型:工具的选择
  • GWT 初体验
  • java.util.concurrent 并发包诸类概览
  • 从 DCL 的对象安全发布谈起
  • 不同团队的困惑
  • 不适合 Hadoop 解决的问题
  • 留心那些潜在的系统设计问题
  • 再谈大楼扔鸡蛋的问题
  • 几种华丽无比的开发方式
  • 我眼中的工程师文化
  • 观点的碰撞
  • 谈谈盗版软件问题
  • 对几个软件开发传统观点的质疑和反驳
  • MVC 框架的映射和解耦
  • 编程的未来
  • DAO 的演进
  • 致那些自嘲码农的苦逼程序员
  • Java 多线程发展简史
  • 珍爱生命,远离微博
  • 网站性能优化的三重境界
  • OSCache 框架源码解析
  • “ 你不适合做程序员”
  • 画圆画方的故事

近期评论

  • + 1.943624 BTC.NEXT - https://graph.org/Ticket--58146-05-02?hs=9a9c6f8dfe3cdbe0074006e3e640b19b& on 所有文章
  • Anonymous on 闲聊投资:亲自体验和护城河
  • 四火 on 关于近期求职的近况和思考
  • YC on 关于近期求职的近况和思考
  • mafulong on 常见分布式基础设施系统设计图解(四):分布式工作流系统
  • 四火 on 常见分布式基础设施系统设计图解(八):分布式键值存储系统
  • Anonymous on 我裸辞了
  • https://umlcn.com on 资源链接
  • Anonymous on 我裸辞了
  • Dylan on 我裸辞了
© 2025 四火的唠叨 | Powered by Minimalist Blog WordPress Theme
Menu
  • 所有文章
  • About Me
  • 关于四火
  • 旅行映像
  • 独立游戏
  • 资源链接