字段的默认值是否保证在线程中可见?

时间:2016-04-06 08:16:57

标签: java multithreading final volatile

讨论this answer我想知道为什么在分配默认值时我们没有使用同步。

class StateHolder {
    private int counter = 100;
    private boolean isActive = false;

    public synchronized void resetCounter() {
            counter = 0;
            isActive = true;
    }

    public synchronized void printStateWithLock() {
        System.out.println("Counter : " + counter);
        System.out.println("IsActive : " + isActive);
    }

    public void printStateWithNoLock() {
        System.out.println("Counter : " + counter);
        System.out.println("IsActive : " + isActive);
    }
}

此类看起来是线程安全的,因为对其字段的访问由synchronized方法管理。这样,我们所要做的就是安全地发布它。例如:

public final StateHolder stateHolder = new StateHolder();

它可以被视为安全出版物吗?我想不,它不可以。咨询final field semantic(强调我的)我发现唯一保证的是stateHolder引用不是陈旧的。 :

  

只能在该对象后看到对象引用的线程   已经完全初始化,保证看到正确    对象的最终字段 的初始化值。

final字段语义不关心final字段引用的ojbect的状态。这样,另一个线程也可以看到字段的默认值。

问题: 我们如何保证在构造函数或实例初始化程序中分配的字段值的内存一致性?

我认为我们必须声明它们volatilefinal,因为在分配引用和构造函数调用之间没有happens-before关系。但是很多库类都不会以这种方式声明字段。 java.lang.String就是一个例子:

public final class String 
          implements java.io.Serializable, Comparable<String>, CharSequence{
    //...
    private int hash; //neither final nor volatile
    //...
}

1 个答案:

答案 0 :(得分:2)

final可以保证在实例构建后您将看到实例变量的赋值,而无需任何进一步的操作。您只需要确保不要在构造函数中泄漏构造的实例。

volatile还可以保证您将看到为某个实例变量设置的默认值,因为实例变量初始值设定项保证在每个JLS 12.5 Creation of New Class Instances的构造函数结束之前执行

安全发布并非完全无关紧要,但如果您坚持使用其中一种流行的机制来实现它,那么您应该完全没问题。您可以查看Safe Publication and Safe Initialization in Java了解更多有趣的细节。

至于String.hash,它是所谓的良性数据竞赛的一个流行的例子。对hash实例变量的访问允许读写的比赛和两次写入的比赛。为了说明后者,两个线程可以同时:

  • 查看初始值0
  • 决定他们是第一个计算哈希
  • 的人
  • 计算哈希码并写入相同的变量而不进行任何同步

由于两个原因,比赛仍被允许并被认为是良性的:

  1. 不可变String实例的散列码计算是幂等操作。
  2. 保证写入32位值不会撕裂。
  3. 尽管如此,仍然不建议进行良性数据竞赛。请参阅Benign data races: what could possibly go wrong?Nondeterminism is unavoidable, but data races are pure evil