7

假设这个类:

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田野呢?

externalvolatile关键字是否也暗示了happens-beforeab字段的关系?或者由于构造函数期间潜在的语句重新排序,是否有可能导致任何线程看到陈旧的值(0在这种情况下为默认值)?int

换句话说,我应该声明ab final/或volatile防止 JMM 出现任何意外,还是仅仅volatile在实例的引用上表明就足够了?

----------------更新后的帖子 - 一个很好的答案: ---------------------------- -

下面的文章通过它的样本证实了这一点,在我的例子中,a并且b受到 JMM 优化的保护,这些优化可以防止永久发生之前的关系。

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

4

6 回答 6

4

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

为简单起见假设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仅适用于变量(例如,x并且y不会volatile因为instanceis 而自动)。这应该从JLS 8.3.1.4

于 2012-11-14T00:11:21.343 回答
4

声明instanceasvolatile不会使其成为 fields volatile,但如果我正确理解了您的问题,那么 - 是的,在您的情况下就足够了。

根据规范的§17.4.5

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

所以,如果一个线程感知到instance已经被初始化,那么它的初始化instance 发生在它之前,而instance' 的字段的初始化发生在它之前,所以线程会感知到instance' 的字段已经被初始化。

于 2012-11-14T00:27:16.560 回答
2

volatile您的情况下,仅适用于AmlThreadSafe. 您仍然必须创建实例变量 ( aand b)或在块volatile中访问它们。synchronized否则你会得到陈旧的数据。

于 2012-11-14T00:14:01.323 回答
2

是的。

thread 1                 thread 2

1 write(a)

2 write(instance)

                         3 read(instance)

                         4 read(a)

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

由于happens-before是传递的,我们有hb(1,2), hb(2,3), hb(3,4),因此hb(1,4)

于 2012-11-14T00:56:22.260 回答
0

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

但是,如果a并且b可以在构造函数之后进行修改,那么这里的其他答案是正确的 - 您需要围绕它们进行同步。

如果您要假设a并且b不会在构造函数之外进行修改,那么为了安全起见,没有理由不将它们设为最终状态。

于 2012-11-14T00:16:36.123 回答
0

例子 :

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条件,现在再次访问锁发现实例== null,调用Something构造函数。现在进入构造函数体内。

只要 Thread#1 进入 Constructor 主体,就会发生上下文切换,现在 Thread#2 轮到执行。

Thread#2 :
调用get Instance,但突然发现instance不是null?为什么{原因会在这之后讨论}因此将部分构造的Object分配给引用并返回它。

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

为什么在Thread#2的情况下返回部分构造的对象引用?原因很简单:语句重新排序。对象创建和引用关联的步骤很简单:

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

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

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

一旦发生上述两个步骤并且如果发生上下文切换,那么引用将指向未初始化的对象,或者可能是内部构造函数主体上下文切换发生的情况,那么在这种情况下,引用将引用部分初始化的对象。

如果发生这种情况,那么引用既不为空也不完整,因此它将破坏我们的单例动机。

现在Volatile如何将我们的生命从这种尴尬中拯救出来:
众所周知,Volatile 的工作有两个原则:1)可见性2)发生在关系之前。现在发生在关系出现之前。

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

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

步骤 3 具有可变变量写入,并且根据之前发生的情况 .. 写入的所有语句都保证可用于步骤 3。并且由于它是可变的,因此在 volatile 和非 volatile 语句之间不会发生重新排序,这在旧 Java 中不是这种情况记忆模型。
所以在执行第 3 步之前,第 1 步和第 2 步保证会发生并可供第 3 步使用。{第 1 步和第 2 步发生的顺序我们不关心它。}

所以从这个 Thread 将看到 Completely created Object 或 null

于 2015-01-11T15:54:58.223 回答