【问题标题】:Why is publishing final fields safe?为什么发布 final 字段是安全的?
【发布时间】:2016-04-02 01:16:17
【问题描述】:

我目前正在阅读Java Concurrency in Practice by Brian Goetz。在第 51 页。在其中一个脚注中,他说:

虽然在构造函数中设置的字段值似乎是第一个 写入这些字段的值,因此没有“旧” 值视为陈旧的值,Object 构造函数首先写入 在子类构造函数运行之前所有字段的默认值。它是 因此可以将字段的默认值视为陈旧的 价值。

所以,我现在还不清楚最终字段的概念。考虑示例类:

public class MyClass{
    private final MyImmutableClass mic;

    public MyClass(){
        mic = MyImmutableClass.empty();
    }
}

根据上面的脚注,mic 字段被分配了两次,一次由Object 的构造函数分配,一次由MyClass 分配构造函数本身。现在,假设我们不安全地发布了一个MyClass 对象(例如通过public 字段):

public final MyClass mc;

谁保证mc 总是被任何处于一致状态的线程观察到?为什么有些线程不能意外观察到默认值?

据我所知,final 字段本身仅保证在对象构造后无法分配引用。如果我们声明mc volatile,那就很清楚了。任何读取该字段的线程都应该直接从内存中读取它。禁止从缓存中读取。

UPD:发布示例:

public static void main(String[] args){
    class MyRunnable implements Runnable(){
        private SomeClass sc;
        public MyRunnable(SomeClass sc){
            this.sc = sc;
        }
        public void run(){
            //do some with sc
        }
    }
    SomeClass sc = getInitialized();
    ExecutorService es = Executors.newFixedThreadPool(10);
    MyRunnable mr = new MyRunnable(sc);
    //submiting mr to es 10 times
    es.awaitTemination();
    es.shutdown();
}

private static SomeClass getInitialized(){
    SomeClass sc = new SomeClass();
    sc. initialize();
    return sc;
}
public class SomeClass
    public MyClass mc;

    public void initialize(){
        mc = new MyClass();
    }
}

SomeClass 实例将跨多个线程发布。某些线程可以观察mic字段的默认值吗?

【问题讨论】:

  • 技术上,Brian Goetz 是不正确的,他说“对象构造函数首先将默认值写入所有字段”。 JLS section 12.5 描述了如何创建新实例,并且“新对象中的所有实例变量,包括在超类中声明的变量,都被初始化为其默认值(第 4.12.5 节)”发生在 之前构造函数,包括超类java.lang.Object的构造函数,被调用。
  • 再一次,这并没有什么不同,因为在写入默认值之后要调用的下一个东西是 java.lang.Object 构造函数
  • @ErwinBolwidt 很棒的评论,非常有趣。非常感谢!!
  • 我应该限定它。尽管 12.5 并不暗示发生之前的关系,但由于 17.4.4,在任何情况下都有一个:“将默认值(零、假或空)写入每个变量与每个变量中的第一个操作同步线。”并且由于在线程上调用构造函数,并且 x 与 y 同步意味着 x 发生在 y 之前,这也意味着默认变量的分配和 java.lang.Object 超类的调用之间存在发生之前的关系构造函数。
  • 我花了一些时间阅读java规范,现在我相信它也会首先初始化为默认值,然后执行初始化程序。

标签: java multithreading final


【解决方案1】:

mc 在您的示例中是一个实例变量。这意味着您必须有一个包含mc 的类的完全初始化实例,以便任何访问某个实例的mc 的代码不会抛出NullPointerException。所以mc在被访问的时候肯定会被初始化。

【讨论】:

  • 所以,如果我们之前没有访问过一个变量,那么就没有线程本地缓存,所以我们必须直接从内存中读取它。对吗?
  • @St.Antario 我不确定我是否理解您的评论。在您的示例中,哪个代码将访问mc?你能显示一些我可以评论的代码sn-p吗?
  • 我添加了发布的例子。
  • @St.Antario 你没有显示你在新的 sn-p 中调用 initialize 的位置
【解决方案2】:

...Object 构造函数首先将默认值写入所有 子类构造函数运行之前的字段...

Object 类构造函数看不到仅属于MyClassMyClass 成员(不是从Object 继承的)。所以上面的说法是正确的,Object类不能实例化成员变量mic

...根据上面的脚注,mic字段被分配了两次, 一次由 Object 的构造函数,一次由 MyClass 的构造函数 本身...

没有。 Object 构造函数只初始化它的成员变量。然后MyClass 构造函数将初始化它为mic。最后,您将拥有MyClass 实例。因此,即使 mic 不是最终的,mic 也不会被分配两次。

发布示例:代码 sn-p 未完成。但是,跨多个线程访问某些内容取决于许多事情,例如 它是否是静态成员?父级在某处被引用为静态成员?在哪里和mc 何时初始化?(默认值 null 肯定是通过构造函数)。 如果不是静态成员,应该是单例,等等……

【讨论】:

  • 我真的不明白你的意思。他说Object的构造函数将默认值写入所有字段......
  • “他”说的是对的。你对 super() 构造函数调用感到困惑......
  • 啊,你的意思是只调用 super() 就会让派生类的所有字段都保留默认值,对吧?
猜你喜欢
  • 2013-11-14
  • 1970-01-01
  • 2012-12-01
  • 2017-03-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多