2

说,我们有代码:

Test t = new Test();

编译成字节码实际上是三个步骤:

1. mem = allocateMem() : allocate memory for Test and save it's address.
2. construct(mem) : construct the class Test
3. t = mem : point t to the mem

在这里我想知道,如果construct(mem) 非常慢,JIT 会在步骤2 中等待直到mem 完全构建好?

如果它不等待(异步)

那么如何保证mem在使用前完全构建(单线程)?

如果它确实等待(同步)

那么为什么双重检查锁(参见下面的代码和本文)会失败?

class DB {
  private DB(){}
  private static DB instance;

  public static DB getInstance() {
    // First check
    if(instance == null ){
      synchronized(DB.class){
        // Second check
        if(instance == null) {
          instance = new Instance();
        }
      }
    }
    return instance;
  }
}

我提到的文章指出,上面的代码将返回一个尚未完全构造的实例。

4

2 回答 2

4

检查我很久以前在 StackOverflow 上给出的这个答案,以了解 DCL 失败的原因以及如何修复它。


问题不在于同步/异步。问题是所谓的重新排序

JVM 规范定义了一种称为发生前关系的东西。在单个线程中,如果语句 S1 出现在语句 S2 之前,则S1 发生在 S2 之前,即 S1 对内存所做的任何修改对 S2 都是可见的。请注意,它并不是说语句 S1 必须在 S2 之前执行。它只是说事情应该看起来好像S1 在 S2 之前执行。例如,考虑以下代码:

int x = 0;
int y = 0;
int z = 0;
x++;
y++;
z++;
z += x + y;
System.out.println(z);

在这里,JVM 执行三个增量语句的顺序无关紧要。唯一的保证是,在运行时z += x + y,x、y 和 z 的值必须全为 1。事实上,如果重新排序不违反happens-before关系,JVM 实际上是允许对语句重新排序的。这样做的原因是有时稍微重新排序可以优化您的代码,并获得更好的性能。

缺点是允许 JVM 以一种在您使用多个线程时可能导致非常奇怪的结果的方式对事物进行重新排序。例如:

class Broken {
  private int value;
  private boolean initialized = false;
  public void init() {
    value = 5;
    initialized = true;
  }
  public boolean isInitialized() { return initialized; }
  public int getValue() { return value; }
}

假设一个线程正在执行这段代码:

while (!broken.isInitialized()) {
  Thread.sleep(1); // patiently wait...
}
System.out.println(broken.getValue());

假设,现在,另一个线程在同一个Broken实例上,

broken.init();

允许 JVM 重新排序init()方法内的代码,首先运行initialized = true,然后才将 设置value为 5。如果发生这种情况,第一个线程,即等待初始化的线程,可能会打印 0!要修复,要么添加synchronized到这两种方法,要么添加volatileinitialized字段。

回到 DCL,单例的初始化可能以不同的顺序执行。例如:

1. mem = allocateMem() : allocate memory for Test and save it's address.
2. construct(mem) : construct the class Test
3. t = mem : point t to the mem

可能变成:

1. mem = allocateMem() : allocate memory for Test and save it's address.
2. t = mem : point t to the mem
3. construct(mem) : construct the class Test

因为,对于单个线程,两个块是完全等价的。也就是说,您可以放心,这种单例初始化对于单线程应用程序是完全安全的。但是,对于多个线程,一个线程可能会获得部分初始化对象的引用!

当您使用多个线程时,要确保语句之间的发生之前的关系,您有两种可能性:获取/释放锁和读取/写入易失性字段。要修复 DCL,您必须声明包含单例的字段volatile。这将确保单例的初始化(即,运行其构造函数)发生在任何读取包含单例的字段之前。有关 volatile 如何修复 DCL 的详细说明,请查看我在此问题顶部链接的答案。

于 2013-06-22T06:28:09.207 回答
2

在这里我想知道,如果construct(mem)非常慢,JIT 会在第 2 步中等待,直到mem完全构建好?

假设您正在谈论由 JIT 生成的代码……那么答案是代码不一定在那时等待。这取决于步骤 3 之后的内容。

那么它如何保证mem在使用前完全构建(单线程)?

要求是线程1中变量的观察值与语言的指定语义一致;即“节目顺序”。如果没有区别,则允许 JIT 重新排序指令。具体来说,如果线程不需要从内存中读取这些变量的值,则某些字段的内存写入是否延迟并不重要。(代码可能根本不需要读取它们,它可以从寄存器中读取它们,或者它可以从 1 级或 2 级缓存中获取它们......)

因此,对“它如何确保它”的简短回答是,它通过按照满足实际语言要求的顺序发出指令来实现它......而不是您提出的更具限制性的语义。


我将您问题的第二部分(关于 DCL 实现)视为没有实际意义。


1 - 这仅适用于该线程。JLS 声明对于其他线程的一致性没有这样的要求……除非在写入和后续读取事件之间存在“先发生”关系。

于 2013-06-22T06:39:20.257 回答