java - JIT如何调度字节码的执行?

标签 java jvm jit double-checked-locking

说,我们有代码:

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在使用前完全构造好(单线程)?

如果确实等待(同步)

那为什么双重检查锁(参见下面的代码和这个 article)失败了?

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;
  }
}

我引用的文章指出上面的代码将返回一个尚未完全构建的实例。

最佳答案

检查 this answer I gave here on StackOverflow很久以前就解释了 DCL 失败的原因以及如何修复它。


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

JVM 规范定义了一种称为发生在 关系的东西。在单个线程内,如果语句 S1 出现在语句 S2 之前,则 S1 happens-before 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。事实上,如果重新排序不会违反发生在 关系。这样做的原因是有时稍微重新排序可以优化您的代码,并获得更好的性能。

缺点是当您使用多线程时,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 添加到这两种方法,或者将 volatile 添加到 initialized 字段。

回到 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

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

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

关于java - JIT如何调度字节码的执行?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17247881/

相关文章:

java - 是否可以从 DTD 自动生成 Java 类?

java google Drive API无法下载二进制文件

java - 当@JacksonXmlProperty.localName 匹配@JacksonXmlRootElement.localName 时无法反序列化展开的列表

jvm - 端口绑定(bind)异常启动activemq

Scala - "companion contains its own main method, which means no static forwarder can be generated"的含义

c++ - JIT 编译能比编译时模板实例化运行得更快吗?

java - HTTPServletRequest getServerPort 返回 -1

Java 世界的本地化方法 - 有什么隐含的含义吗?

jvm - 有没有办法关闭 JIT 编译器,这样做是否会影响性能?

inheritance - 内联私有(private)和 protected 虚函数调用