最终的关键字在并发性方面保证了什么?

我想我已经读了一个字段的最后一个关键字保证,如果线程1实例化包含该字段的对象,则线程2将始终看到该字段的初始化值,如果线程2有一个对象的引用(假定它是正确构build)。 它也在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 } } 

你说的是真的。

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

最后的关键字有更多的好处。 从Java实践中的协调:

最终字段不能被修改(尽pipe它们引用的对象可以被修改,但是它们在Java内存模型下也有特殊的语义)。 最终字段的使用可以保证初始化的安全性(见第3.5.2节),使不可变对象可以自由访问和共享,而不需要同步。

书中说:

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

  • 从静态初始化程序初始化一个对象引用;
  • 将引用存储到易失性字段或AtomicReference中;
  • 将引用存储到正确构造的对象的最终字段中; 要么
  • 将引用存储到一个由锁保护的字段中。

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

给定一个写w ,一个冻结f ,一个动作a (不是最后一个字段的读取),一个由f冻结的最后一个字段的读r1和一个读r2 ,使得hbwf ), hbfa ), mcar 1和解引用r 1r 2 ),那么当确定r 2可以看到哪些值时,我们考虑hbwr 2 )。

我们来分析一下这个问题:

  • w :线程1写入bNotFinal
  • f :冻结b的冻结动作
  • a :发布A对象引用
  • r 1 :通过线程2读取b (由f冻结)
  • r 2 :线程2对b.bNotFinal的b.bNotFinal

我们注意到

  • hbwf ):写入bNotFinal发生在b的冻结之前
  • hbfa ):构造函数完成后(即冻结之后)发布A引用,
  • mcar 1 ):线程2在读取Ab之前读取A引用
  • 取消引用r 1r 2 ):线程2通过访问b.bNotFinal取消引用b的值

下列句子…

“那么当确定r 2可以看到哪些值时,我们考虑hbwr 2 )”

然后转化为…

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

即线程2保证看到b.bNotFinal的值2


Bill Pugh的相关引用 :

能够看到正确构造的字段值是很好的,但是如果字段本身是一个引用,那么你也希望你的代码看到它所指向的对象(或数组)的最新值。 如果你的领域是最后一个领域,这也是有保证的。 因此,可以有一个指向数组的最终指针,而不必担心其他线程看到数组引用的正确值,但数组内容的值不正确。 同样,在这里,“正确”是指“最新的对象构造函数的末尾”,而不是“最新的可用值”。

特别是这个直接回答了关于String引用的同步共享的例子@supercat。

 class A { private final B b = new B(); } 

上面的行只保证当你从A的一个实例访问b时,b将被初始化。现在b的初始化或者B的任何实例化的细节完全依赖于如何定义B,在这种情况下它可以按照JLS进行初始化或者可能不。

所以,如果你做A a = new A(); 从一个线程,并以某种方式设法从另一个线程读取ab,这是保证你不会看到null如果a不是null,但b.bNotFinal可能仍然是零。

值得一提的是,这里的final一个目标与线程对值的可见volatile起的作用是一样的。 也就是说,你不能在一个领域使用finalvolatile ,因为他们是相互冗余的。 回到你的问题。 正如其他人指出的那样,你的假设是错误的,因为JLS只保证引用B的可见性,而不是B中定义的非最终字段。但是,可以使B的行为按照你希望的方式行事。 一个解决scheme是宣布bNotFinal如果不能final bNotFinalvolatile

“JSR 133(Java存储器模型)FAQ,Jeremy Manson和Brian Goetz,2004年2月”介绍final字段如何工作。

  • 最终字段如何显示更改其值?
  • 新JMM下的最终字段如何工作?

引用:

JSR 133的目标包括:

  • 应提供初始化安全性的新保证。 如果一个对象被正确地构造了(这意味着在构造过程中对它的引用不会被转义),那么所有看到该对象引用的线程都将看到在构造器中设置的最终字段的值,而不需要同步。

在最终分配 (这是一个存储围栏) 后插入内存屏障 ,这是如何保证其他线程将看到您分配的值。 我喜欢JLS以及它如何说事情是在发生之前/之后完成的,并且保证了最后的决定,但对我来说,内存屏障和效果要简单得多。

你应该真的读到这个: 内存障碍