最终关键字对并发的保证究竟是什么?

时间:2014-12-02 16:23:58

标签: java multithreading concurrency

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

  

[Thread 2]也会看到引用的任何对象或数组的版本   那些最终字段至少与最终字段一样是最新的   是。 (section 17.5 of JLS)

这意味着如果我有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
  }
}

6 个答案:

答案 0 :(得分:16)

你说的是真的。

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

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

  

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

书籍当时说:

  

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

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

答案 1 :(得分:12)

我认为您的问题在Section 17.5.1: Semantics of final FieldsA relevant quote引用的部分正下方由JLS回答:

  

给定 w ,冻结 f ,动作 a (不是读取最终字段),读取由 f 冻结的最终字段的 r 1 ,以及读取 r 2 这样 hb w f ), hb f a ), mc a r 1 )和 dereferences < / em>( r 1 r 2 ),然后确定 r 2 ,我们考虑 hb w r 2 )。

让我们根据问题对其进行细分:

  • w :是线程1
  • bNotFinal的写入
  • f :冻结b
  • 的冻结操作
  • a :发布A对象参考
  • r 1 :线程2读取b(由 f 冻结)
  • r 2 :线程2的b.bNotFinal聋人

我们注意到

  • hb w f ):bNotFinal的写入在冻结b之前发生/ LI>
  • hb f a ):A引用在构造函数完成后发布(即冻结后) )
  • mc a r 1 ):线程2在之前读取A引用阅读A.b
  • 取消引用 r 1 r 2 ):线程2解除引用访问b
  • b.bNotFinal的价值

以下句子......

  

&#34;然后在确定 r 2 可以看到哪些值时,我们会考虑 hb w r 2 )&#34;

...然后转换为

  

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

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


Bill Pugh的

{{3}}:

  

能够看到字段正确构造的值很好,但如果字段本身是引用,那么您还希望代码查看它指向的对象(或数组)的最新值。如果您的字段是最终字段,则也可以保证。因此,您可以拥有一个指向数组的最终指针,而不必担心其他线程看到数组引用的正确值,但是数组内容的值不正确。再次,通过&#34;纠正&#34;在这里,我们的意思是&#34;最新的对象构造函数的结束&#34;,而不是&#34;最新值&#34;。

特别是这是关于String引用的错误同步共享的示例@supercat的直接答案。

答案 2 :(得分:3)

值得一提的是,final在线程值的可见性方面与volatile具有相同的用途。也就是说,您不能在字段上同时使用finalvolatile,因为它们彼此是多余的。回到你的问题。正如其他人指出你的假设是错误的,因为JLS只保证B的引用的可见性,而不是B中定义的非final字段。但是,你可以使B的行为方式与你想要的行为一致。一种解决方案是将bNotFinal声明为volatile,如果它不能final

答案 3 :(得分:2)

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

上面的行只保证当你从A的实例访问b时b将被初始化。现在b的初始化或B的任何实例化的细节完全取决于B的定义方式,在这种情况下它可能被初始化根据JLS或可能不会。

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

答案 4 :(得分:0)

“JSR 133(Java Memory Model)FAQ,Jeremy Manson和Brian Goetz,2004年2月”描述了final现场工作的方式。

QUOTE:

  

JSR 133的目标包括:

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

答案 5 :(得分:0)

在最终分配(它是商店围栏)后插入了内存屏障,这就是您保证其他线程将看到您分配的值的方式。我喜欢JLS以及它是如何完成事情 - 在事件之前/之后发生事情并保证决赛,但对我而言,记忆障碍及其影响更容易掌握。

你应该读到这个:Memory barriers