【问题标题】:Java: Final field freeze on object reachable from final fieldsJava:从最终字段可访问的对象上的最终字段冻结
【发布时间】:2010-12-15 11:09:37
【问题描述】:

标题为“Core Java Concurrency”的 DZone 参考卡指出:

一旦设置,最终字段值 无法更改。将对象引用字段标记为 final 不会阻止从该字段引用的对象稍后更改。为了 例如,最终的 ArrayList 字段不能更改为不同的 ArrayList,但可以在列表实例上添加或删除对象。

和

最终字段冻结不仅包括对象中的最终字段,还包括所有 可从这些最终字段访问的对象。

我对第二个说法并不完全清楚。这是否意味着如果我在 B 类类型的 A 类中有一个 final 字段,而后者又具有一个 Integer 类型的 final 字段,那么 A 类实例的最终字段冻结仅在 b.c 的最终字段冻结后完成已经发生了吗?

public class A{

  public final B b = new B();

}

public class B{ 

  public final Integer c = 10;

}

【问题讨论】:

    标签: java concurrency


    【解决方案1】:

    这是否意味着如果我有一个决赛 B 类类型 A 类中的字段, 它又具有最终字段 输入整数,然后冻结最终字段 对于 A 类的实例完成 只有在最后的字段冻结后 b.c 已经发生了吗?

    我想我会小心地说,在这种情况下,最终字段冻结意味着当您创建 A 的实例并安全地发布它时,其他对象将永远不会看到 b 或 c 的未初始化值。

    我还要说,当您在 A 中创建 B 的实例时,A 中的其他初始化代码将永远不会看到 c 的未初始化值。

    我遇到有关最终字段冻结的真正问题的一个案例是,例如,一个包含(可变)HashMap 的类,仅用于读取,在构造期间初始化:

    public class DaysOfWeek {
        private final Map daysOfWeek = new HashMap();
        public DaysOfWeek() { 
          // prepopulate my map
          daysOfWeek.put(0, "Sunday");
          daysOfWeek.put(1, "Monday");
          // etc
        }
    
        public String getDayName(int dayOfWeek) {
          return daysOfWeek(dayOfWeek);
        }
    }
    

    这里出现了问题:假设这个对象是安全发布的,并且考虑到这里没有同步,其他线程调用getDayName()是否安全?答案是肯定的,因为 final 字段冻结保证了 HashMap 和所有可从它到达的东西(这里只是字符串,但可以是任意复杂的对象)在构造结束时被冻结。 [如果您想在构建后实际修改此映射,那么您将需要围绕读取和写入进行显式同步。] 这是一个 lengthier blog 探索该主题并检查 cmets 以获取像 Brian Goetz 这样的人的一些有趣的回复。

    顺便说一句,我是 refcard 的作者

    【讨论】:

    • 感谢 Alex 的详细回答。非常感谢。顺便说一句,我也很喜欢 Refcard。
    • >>...在这种情况下,最终字段冻结意味着当您创建 A 的实例并安全地发布它时,其他对象将永远不会看到 b 或 c 的未初始化值
    • @Alex Miller “Final field freeze”是官方术语?
    • @AlexMiller 如果只有该规范是人类可读的......我已经尝试阅读它几次,仍然没有运气。我认为这个冻结动作在这里:给定一个写入 w,一个冻结 f,一个动作 a(不是读取最终字段),读取 r1 冻结的最终字段通过 f 和读取 r2 使得 hb(w, f)、hb(f, a)、mc(a, r1) 和 dereferences(r1, r2),然后在确定 r2 可以看到哪些值时,我们考虑 hb(w, r2)
    【解决方案2】:

    Java Concurrency in Practice 在第 16.3 节中提到了这一点:

    初始化安全保证 对于正确构造的对象, 所有线程都会看到正确的 设置的最终字段的值 由构造函数,不管如何 对象已发布。此外,任何 可以达到的变量 通过适当的最后一个字段 构造对象(例如 最终数组的元素或 a 引用的 HashMap 的内容 final 字段)也保证是 对其他线程可见。对于对象 带有最终字段,初始化 安全禁止重新订购任何零件 初始荷载下的施工 对该对象的引用。全部 写入由 构造函数,以及任何 变量可通过那些 字段,变得“冻结”时 构造函数完成,并且任何线程 获得对那个的引用 保证对象看到一个值 至少与 冻结值。写入初始化 可通过 final 访问的变量 字段未重新排序 以下操作 施工后冻结。

    【讨论】:

    • 作者所说的“无论对象如何发布”到底是什么意思?我相信正确构建本身意味着其引用不会在构建过程中逃脱。
    【解决方案3】:

    没错。这来自JMM

    寻找段落:

    当一个对象的构造函数完成时,它被认为是完全初始化的。只有在对象完全初始化后才能看到对该对象的引用的线程可以保证看到该对象的最终字段的正确初始化值。

    因为在 B 类初始化保证 B.c 冻结之前构造函数不会完成

    【讨论】:

      【解决方案4】:

      保证比您想象的要强大。 final 字段语义甚至适用于分配给 final 字段的可变对象(具有通常的限制)。所以扩展您的示例以使 A.b 私有和 B 可变(但在外部不可变)。

      public class A {
          private final B b = new B();
          public Integer get() { return b.c; }
      }
      
      public class B {
          public Integer c = 10;
      }
      

      在这种情况下,A.get 将永远不会返回 null,即使在不安全的发布下也是如此。当然,这个例子是完全抽象的,因此毫无意义。通常它对数组(例如String)和集合很重要。

      【讨论】:

      • 如果你把你的最终初始化放在构造函数的末尾并且有一个错误的代码从构造函数中泄漏了这个,那么你可以得到 null
      【解决方案5】:

      在其他什么之前谈论什么是最终的并没有什么意义。对于您的程序,一旦您的对象被创建(实际上是从该字段被分配一次的那一刻起),引用就不能再改变了。 由于 B 实例是在 A 实例之前创建的,因此您可以说 c 在 b 之前成为 final,但这并不重要。

      当您在一个类中有多个最终字段时,顺序很重要。如果您想在另一个最终字段的赋值中使用一个 final 字段的值,您应该只访问已经初始化的字段。

      说实话,“final field freeze”这句话对我来说没有多大意义。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-02-24
        • 2020-09-28
        • 2012-02-06
        • 2019-05-13
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多