参考类型的易失性 – 是否总是避免因JMM而导致出现参考问题?

假设这堂课:

public class AmIThreadSafe { private int a; private int b; AmIThreadSafe(int a, int b) { this.a = a; this.b = b; } } 

假设实例对此类的引用(声明为volatile )可以被一些线程访问(导致竞争条件),只要this (引用)转义:

 volatile AmIThreadSafe instance = new AmIThreadSafe(1,2); 

在这里,我确信分配instance引用的事实发生在线程读取之前

AmIThreadSafe's领域呢?

外部volatile关键字是否也意味着关于ab字段happens-before关系? 或者由于在构造函数中重新排序的潜在语句,是否有可能最终得到任何线程看到过时的值(在这种情况下,因为int默认值为0 )?

换句话说,我应该声明ab finalvolatile来防止JMM出现任何意外,或者只是在实例的引用上指示volatile吗?

—————- 更新的post – 一个好的答案: ————————— –

下面的文章通过其样本证实,在我的情况下, ab受到保护,不受JMM优化的影响,这些优化会阻止永久发生之前的关系。

http://jeremymanson.blogspot.fr/2008/11/what-volatile-means-in-java.html

instance声明为volatile不会使其字段变得volatile ,但如果我正确理解您的问题,那么 – 是的,在您的情况下就足够了。

根据规范的§17.4.5 :

  • 一个线程中的volatile写入发生在另一个线程中的任何后续volatile读取之前
  • 同一个线程中的语句具有您期望的之前发生的关系。
  • 发生在关系传递之前

因此,如果一个线程感知instance已被初始化,那么instance的初始化发生在之前 ,并且instance的字段的初始化发生在之前 ,因此线程将感知instance的字段已经初始化。

不,它不足以让它变得不稳定。 线程安全取决于使用情况。 例如,如果另一个线程正在修改值,这仍然会产生意外的结果。

为简单起见假设public变量

 volatile AmIThreadSafe instance = new AmIThreadSafe(1,2); if (instance.x == 0) { // instance.x might have changed between checking and assigning instance.x = instance.x + 1; } 

volatile仅适用于变量(例如, xy不会因为instance而自动volatile )。 这应该从JLS 8.3.1.4清楚

您的案例中的volatile仅适用于AmlThreadSafe的引用。 您仍然必须使实例变量( abvolatile或者在synchronized块中访问它们。 否则,您可以获得过时的数据。

是。

 thread 1 thread 2 1 write(a) 2 write(instance) 3 read(instance) 4 read(a) 

由于实例是易变的,[2]发生在[3]之前。

由于发生之前是传递性的,我们有hb(1,2),hb(2,3),hb(3,4),因此hb(1,4)

如果ab仅在构造函数中被修改,那么在这种情况下你应该没问题,因为在将引用分配给instance之前创建了对象(并设置ab ),并且任何其他线程都不会在本地缓存这些位置的内存副本,因为它是线程以前无法看到的新对象。 换句话说,我不相信另一个线程可能会看到ab的“默认”值为0,因为构造函数将在将对象的引用分配给instance之前完全运行。

但是,如果可以在构造函数之后修改ab ,那么此处的其他答案是正确的 – 您需要围绕它们进行同步。

如果你假设ab不会在构造函数之外被修改,那么就没有理由不让它们最终,只是为了安全。

示例:

 class Something{ private volatile static Something instance = null; private int x; private int y; private Something(){ this.x = 1; this.y = 2; } public static Something getInstance() { if (instance == null) { synchronized (Something.class) { if (instance == null) instance = new Something(); } } } return instance; } } 

说明:


假设我们有上述代码:

现在假设实例在一段时间内不易变化:

线程#1
进来调用getInstance方法,检查实例值{since null},将进入IF条件,现在再次访问Lock找到instance == null,调用Something构造函数。 现在进入构造函数体。

一旦Thread#1进入Constructor体内,就会发生Context Switch,现在Thread#2开始执行。

线程#2
调用get实例,但是突然发现实例不是null?为什么{Reason将在此之后讨论}并因此将部分构造的Object分配给引用并返回它。

现在情况是这样的: 线程#1仍然需要完全构造对象{需要完全构造它}并且线程#2获得部分构造的对象的引用,如果它使用它就像说reference.x //将打印“x “默认值而不是”1“

为什么在线程#2的情况下返回部分构造的对象引用? 原因很简单: 声明重新排序 。 步骤对于对象创建和引用关联来说很简单:

  1. 在堆中分配内存。
  2. 执行构造函数体,它将初始化类成员。
  3. 完成上述步骤后,参考新创建的对象。

但有时编译器可以按顺序执行这些指令,这意味着:
可能会发生这样的事情:

  1. 在堆中分配内存。
  2. 引用新创建的Object。
  3. 执行构造函数体,它将初始化类成员。

一旦上述两个步骤发生并且如果上下文切换发生,则引用将指向未初始化的对象,或者可能是Inside Constructor Body Context Switch发生的情况,那么在该情况下引用将引用部分初始化的Object。

如果出现这种情况,那么引用既不会为空也不会完整,因此它将破坏我们的单身人士动机。

现在, 挥发性将如何从这些尴尬中拯救我们的生命:
正如我们所知,挥发性工作有两个原则:1) 可见性 2) 在关系之前发生 。 现在发生在关系进入图片之前

所以引用是volatile,所以所有语句都应该在任何Volatile写入之前发生。如果我们看看我们的Object构造步骤:

  1. 为Object分配内存
  2. 初始化成员变量{Constructor Body}
  3. 将对象引用分配给volatile变量实例。

步骤3具有易失性变量写入和按照之前发生的情况。所有语句写入保证可用于步骤3.并且因为它是易失性因此在易失性和非易失性语句之间不会发生重新排序,这在旧Java中不是这样的记忆模型。
因此,在执行步骤3之前,步骤1和步骤2保证会发生并可用于步骤3. {在第1步和第2步的顺序中,我们不打扰它。}

因此,从这个线程将看到完全创建的对象或null