【问题标题】:Java reordering and volatile issueJava 重新排序和易失性问题
【发布时间】:2014-12-19 11:54:10
【问题描述】:

我最近遇到了一个有趣的问题:

例如我有A类:

class A {
  int a;
  int b;

  public A() {
    a = 1;
    b = 2;
  }

  public int getA() {return a;}
  public int getB() {return b;}
}

A 类只能作为单例存在; 因此,为了提供对单例的访问,我创建了类 Factory:

class Factory {
private A a;
public A getA() {
    if (a == null) {
        synchronized(this) {
            if (a == null) {
                a = new A();
            }
        }
    }
    return a;
}

}

据我了解,根据 JVM 中的重新排序,如果 2 个线程同时访问 Factory.getA(),则其中一个线程可能会获得部分构造的对象,这可能导致应用程序崩溃。

但如果我将 A a 设为私有; volatile 我可以确定每个线程只会访问完全构造的对象吗?

因此,作为结论,如果我将变量 x 标记为 volatile,它会影响 x.class 构造函数的内容吗?

【问题讨论】:

    标签: java multithreading volatile


    【解决方案1】:

    但如果我将 A a 设为私有; volatile 我可以确定每个线程只会访问完全构造的对象吗?

    是的。

    考虑线程#1 初始化a 的情况。工厂互斥锁确保一次只有一个线程可以创建A。所以我们有以下序列:

    1. 线程 #1 发现 a 为空

    2. 线程 #1 获取互斥体。

    3. 线程 #1 参见 a 仍然为空。

    4. 线程#1 构造一个A 实例,用于初始化其变量。

    5. 线程 #1 将实例引用分配给 a

    6. 线程 #1 释放锁。

    如果线程#2 看到a 处于非空状态(例如,在步骤#1 或步骤#3 测试中),这意味着线程#1 必须(至少)到达步骤# 5.但这意味着

    A.a = 1;                    // thread #1
    

    “发生在之前”

    A.b = 2;                    // thread #1
    

    “发生在之前”

    write to Factory.a          // thread #1
    

    “发生在之前”

    read from Factory.a         // thread #2
    

    “发生在之前”

    access to some field of `A` // thread #2
    

    因为从线程#1 中新A 的字段的初始化到线程#2 中的字段访问,存在完整的“发生在”关系链。这意味着线程#2 保证可以看到字段初始化写入的结果...除非有其他干预这些字段的写入。

    请注意,在写入易失性a 和随后在另一个线程中读取之间的“发生之前”是关键。如果a 不是易失性的,那么就没有完整的“发生在”链并且因此执行不是“一致的发生之前”;即它包含“数据竞争”。

    我在上面使用/遵循的术语和推理来自 Java 语言规范,Chapter 17.4

    【讨论】:

    • 这是正确答案!。很好的解释斯蒂芬
    【解决方案2】:

    易变

    Java volatile 关键字用于将 Java 变量标记为“正在存储在主内存中”。更准确地说,对 volatile 变量的每次读取都将从计算机的主内存中读取,而不是从 CPU 缓存中读取,并且对 volatile 变量的每次写入都将写入主内存,而不仅仅是 CPU 缓存.

    它与记忆有关。它不会影响构造函数。

    来源:点击here

    【讨论】:

    • 谢谢,请您指教我如何确保任何线程只会获得完全构造的对象?
    • 至少将 credit 归功于您借用此内容的source
    • @TheLostMind 没错。我忘记链接了
    【解决方案3】:

    由于您希望 A 成为单身人士,我的建议是让 A 成为枚举。
    使用枚举:

    • 将解决您的问题,因为您永远不会有部分构造的枚举
    • 而且使用枚举也是实现单例模式的最佳方式。

      enum A{
         INSTANCE(1,2);
      
         private int a;
         private int b;
      
         A(int a,int b){
             this.a=a;
             this.b=b;
          }
      
         public int getA(){ return a; }
         public int getB(){ return b; }
      }
      

    【讨论】:

    • 我知道这可能与枚举有关,但我只是对“易失性”的行为感兴趣
    • 那么我建议您将问题更改为特定于 volatile 的行为
    【解决方案4】:

    有两种不同的事物规范和实现。如果你让a volatile,那么在实践中就会解决这个问题,因为JVM会在赋值后插入一个存储屏障。但这仍然会破坏规范,因为volatile 不像final,它不能保证一个对象的所有子对象都会被发布。顺便说一句,在具有强内存模型的 CPU 上,很多糟糕的代码都可以正常工作,但这并不意味着你应该这样写。

    【讨论】:

      猜你喜欢
      • 2023-03-21
      • 1970-01-01
      • 2017-08-02
      • 1970-01-01
      • 2016-05-31
      • 2012-02-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多