关于并发性,最终关键字究竟保证了什么?

2022-09-01 00:39:43

我想我已经读到,字段上的最后一个关键字保证,如果线程1实例化包含该字段的对象,那么如果线程2具有对该对象的引用(前提是它已正确构造),则线程2将始终看到该字段的初始化值。它还在JLS中说

[线程 2] 还将看到由最终字段引用的任何对象或数组的版本,这些字段至少与最终字段一样最新。(JLS第17.5节)

这意味着如果我有A类

class A {
  private final B b = new B();
  private int aNotFinal = 2;
  ...

和 B 类

class B {
  private final int bFinal = 1;
  private int bNotFinal = 2;
  ...

则 aNotFinal 不能保证在线程 2 获取对类 A 的引用时进行初始化,但字段 bNotFinal 是,因为 B 是由 JLS 中指定的最终字段引用的对象。

我有这个权利吗?

编辑:

发生这种情况的一种情况是,如果我们有两个线程同时在类C的同一实例上执行getA()

class C {
  private A a;

  public A getA(){
    if (a == null){
      // Thread 1 comes in here because a is null. Thread B doesn't come in 
      // here because by the time it gets here, object c 
      // has a reference to a.
      a = new A();
    }
    return a; // Thread 2 returns an instance of a that is not fully                     
              // initialized because (if I understand this right) JLS 
              // does not guarantee that non-final fields are fully 
              // initialized before references get assigned
  }
}

答案 1

你说的没错。

将字段标记为 final 将强制编译器在构造函数完成之前完成字段的初始化。但是,对于非最终字段,没有这样的保证。这可能看起来很奇怪,但是编译器和JVM出于优化目的(例如重新排序指令)做了很多事情,导致这样的事情发生。

最终关键字还有更多好处。来自 Java Concurecncy in Practice:

最终字段不能被修改(尽管它们引用的对象如果是可变的,则可以修改),但它们在Java内存模型下也具有特殊的语义。正是使用最终字段使得初始化安全的保证成为可能(参见第 3.5.2 节),它允许在没有同步的情况下自由访问和共享不可变对象。

书中说:

若要安全地发布对象,必须同时使对该对象的引用和该对象的状态对其他线程可见。正确构造的对象可以通过以下方式安全地发布:

  • 从静态初始值设定项初始化对象引用;
  • 将对它的引用存储到易失性字段或原子引用中;
  • 将对它的引用存储到正确构造的对象的最终字段中;
  • 将对它的引用存储到由锁正确保护的字段中。

答案 2

我认为你的问题是由JLS在你引用的部分下面回答的,在第17.5.1节:最终字段的语义中

给定一个写 w、一个冻结 f、一个动作 a(不是对最终场的读取)、一个被 f 冻结的最终字段的读取 r1,以及一个读取 r2,使得 hbwf)、hbfa)、mcar1) 和 dereferencesr1r2)),然后当确定r2可以看到哪些值时,我们考虑hbwr2)。

让我们从问题的角度来分解:

  • w:是线程 1 写入bNotFinal
  • f:是冻结的冻结动作b
  • a:发布对象引用A
  • r1:线程 2 读取(由 f 冻结)b
  • r2:按线程 2 的 reafb.bNotFinal

我们注意到

  • hbwf):写入发生在bNotFinalb
  • hbfa):引用在构造函数完成后发布(即在冻结之后)A
  • mcar1):线程 2 在读取之前读取引用AA.b
  • 取消引用r1r2):线程 2 通过访问bb.bNotFinal

下面这句话...

“那么在确定r2可以看到哪些值时,我们考虑hbwr2))"

...然后翻译为

在确定读取时可以看到哪些值,我们认为线程 1 的写入发生在读取 之前b.bNotFinalbNotFinalb.bNotFinal

即,线程 2 保证可以看到 的值。2b.bNotFinal


比尔·皮尤(Bill Pugh)的相关引用

能够查看字段的正确构造值很不错,但如果字段本身是引用,那么您还需要代码查看它所指向的对象(或数组)的最新值。如果您的字段是最终字段,这也得到了保证。因此,您可以拥有指向数组的最终指针,而不必担心其他线程看到数组引用的正确值,但数组内容的值不正确。同样,这里的“正确”是指“截至对象构造函数末尾的最新值”,而不是“可用的最新值”。

特别是,这是对@supercat提出的关于不同步共享引用的示例的直接答案。String


推荐