当前位置: 首页 > 面试题库 >

Java JVM中的指令重新排序

谷良弼
2023-03-14
问题内容

我正在阅读此博客文章。

作者正在谈论在多线程环境中打破hashCode()in String

有了:

public int hashCode() {
     int h = hash;
     if (h == 0) {
         int off = offset;
         char val[] = value;
         int len = count;

         for (int i = 0; i < len; i++) {
             h = 31*h + val[off++];
         }
         hash = h;
     }
     return h;
 }

变成:

public int hashCode() {
     if (hash == 0) {
         int off = offset;
         char val[] = value;
         int len = count;

         int h = 0;
         for (int i = 0; i < len; i++) {
             h = 31*h + val[off++];
         }
         hash = h;
     }
     return hash;
 }

作者说,我引用:

“我在这里所做的是添加一个附加读取: 哈希的第二次读取,在返回之前
。听起来很奇怪,而且不太可能发生,第一次读取可以返回正确计算出的哈希值,内存模型允许第二次读取返回0!这是允许的,因为该模型允许对操作进行广泛的重新排序。第二次读取实际上可以在代码中移动,以便处理器在第一次读取之前进行处理!”

因此,进一步查看评论,有人说可以将其重新排序为

int h = hash;
if (hash == 0) {
  ...
}
return h;

那怎么可能?我认为重新排序仅涉及上下移动程序语句。它遵循什么规则?我已经用Google搜索,阅读了JSR133 FAQ,检查了Java
Concurrency in Practice一书,但是我似乎找不到一个可以帮助我特别了解重新排序的地方。如果有人能指出正确的方向,我将不胜感激。

感谢路易斯澄清了“重新排序”的含义,我不是在考虑“ byteCode”

但是,我仍然不明白为什么允许将第二读移动到最前面,这是我幼稚的尝试,将其转换为某种“字节码”格式。

为了简化起见,用于计算哈希码的操作表示为calchash()。因此,我将该程序表示为:

if (hash == 0)  {       
    h = calchash();
    hash = h;
}
return hash;

我试图以“字节码”形式表示它:

R1,R2,R3 are in the operands stack, or the registers
h is in the array of local variables

程序顺序:

if (hash == 0)  {       ---------- R1 = read hash from memory (1st read)
                        ---------- Compare (R1 == 0)
    h = calchash();     ---------- R2 = calchash()
                        ---------- h = R2 (Storing the R2 to local variable h)
    hash = h;           ---------- Hash = h (write to hash)
}
return hash             ---------- R3 = read hash from memory again(2nd read)
                        ---------- return R3

重新排序的转换(我的版本基于注释):

                        ---------- R3 = read hash from memory (2nd read) *moved*
if (hash == 0)  {       ---------- R1 = read hash from memory (1st read)
                        ---------- Compare (R1 == 0)
    h = calchash();     ---------- R2 = calchash()
                        ---------- h = R2 (Storing the R2 to local variable h)
    hash = h;           ---------- hash = h (write to hash)
}
return hash             ---------- return R3

再次检查评论,我发现作者回答了这一问题:

重新排序的转换(来自博客)

r1 = hash;
if (hash == 0) {
  r1 = hash = // calculate hash
}
return r1;

这种情况实际上适用于单线程,但是可能会因多线程而失败。

看来JVM在简化基于

h = hash and it simplifies the use of R1, R2, R3 to single R1

因此,JVM不仅可以对指令进行重新排序,还可以减少正在使用的寄存器数量。


问题答案:

在修改后的代码中:

public int hashCode() {
     if (hash == 0) { // (1)
         int off = offset;
         char val[] = value;
         int len = count;

         int h = 0;
         for (int i = 0; i < len; i++) {
             h = 31*h + val[off++];
         }
         hash = h;
     }
     return hash; // (2)
 }

(1)和(2)可以重新排序:(1)可以读取一个非null值,而(2)可以读取0。这在String类的实际实现中不会发生,因为计算是在局部变量上进行的返回值也是该局部变量,根据定义,该局部变量是线程安全的。

问题在于,如果在hash没有适当同步的情况下访问共享变量()时,Java内存模型不提供任何保证-
特别是它不能保证所有执行将顺序一致。如果hash是易失性的,则修改后的代码不会有问题。

ps:我相信该博客的作者是JLS第17章(Java内存模型)的作者之一-所以无论如何我都会相信他;-)

更新

在进行各种编辑/注释之后-让我们使用这两种方法更详细地查看字节码(为简单起见,我假设哈希码始终为1):

public int hashcode_shared() {
    if (hash == 0) { hash = 1; }
    return hash;
}

public int hashcode_local() {
    int h = hash;
    if (h == 0) { hash = h = 1; }
    return h;
}

我的机器上的Java编译器生成以下字节码:

public int hashcode_shared();
   0: aload_0                           //read this
   1: getfield      #6                  //read hash (r1)
   4: ifne          12                  //compare r1 with 0
   7: aload_0                           //read this
   8: iconst_1                          //constant 1
   9: putfield      #6                  //put 1 into hash (w1)
  12: aload_0                           //read this
  13: getfield      #6                  //read hash (r2)
  16: ireturn                           //return r2

public int hashcode_local();
   0: aload_0                           //read this
   1: getfield      #6                  //read hash (r1)
   4: istore_1                          //store r1 in local variable h
   5: iload_1                           //read h
   6: ifne          16                  //compare h with 0
   9: aload_0                           //read this
  10: iconst_1                          //constant 1
  11: dup                               //constant again
  12: istore_1                          //store 1 into h
  13: putfield      #6                  //store 1 into hash (w1)
  16: iload_1                           //read h
  17: ireturn                           //return h

在第一个示例中,共有两次读取共享变量hash:r1和r2。如上所述,由于没有同步并且变量是共享的,因此将应用Java内存模型,并允许编译器/
JVM对这两个读取进行重新排序:可以在第1 *行之前插入第13行。

在第二个示例中,h由于对非共享变量的线程内语义和程序顺序保证,对局部变量的所有操作都需要顺序一致。

注意:与往常一样,允许重新排序的事实并不意味着将执行重新排序。实际上,在当前的x86
/热点组合中不太可能发生这种情况。但是它可能会在其他当前或将来的体系结构/ JVM上发生。

*这有点捷径,实际上可能发生的情况是编译器可能会这样重写hashcode_shared

public int hashcode_shared() {
    int h = hash;
    if (hash != 0) return h;
    return (hash = 1);
}

该代码在单线程环境中严格等效(它将始终返回与原始方法相同的值),因此允许重新排序。但是,在多线程环境中,很明显,如果hash前两行之间的另一个线程将其从0更改为1,则此重新排序的方法将错误地返回0。



 类似资料:
  • 变量res的值应等于3。但是当我打开优化时,编译器错误地重新排列了指令,并且res包含一些垃圾。一些可能的重新排序示例: 这是编译器中的错误吗?还是不允许像这样访问结构数据成员? 编辑: 我刚刚意识到之前的代码实际上有效,抱歉。但这不起作用: 当编译时不知道变量i时,编译器会错误地重新排序指令。

  • 问题内容: 我只是碰到一篇文章,声称我从未听过,也找不到其他地方。声称是从另一个线程的角度来看,可以根据构造函数内部的指令对构造函数返回的值的分配进行重新排序。换句话说,声称是在下面的代码中,另一个线程可以读取其中未设置的非空值。 这是真的? 编辑: 我认为从线程执行的角度来看,可以保证的分配与的分配具有先发生后关系。但是,这两个值都可能缓存在寄存器中,并且可能未按照最初写入的顺序将它们刷新到主存

  • 问题内容: 在《 Java Concurrency InPractice》一书中,有几次告诉我们可以通过编译器,运行时JVM甚至处理器来重新排序程序的指令。因此,我们应该假定执行的程序不会以与源代码中指定的顺序完全相同的顺序执行其指令。 但是,上一章讨论的Java内存模型提供了一系列先 发生后 规则的清单,这些规则指示JVM保留哪些指令顺序。这些规则中的第一个是: “程序顺序规则。线程中的每个动作

  • 我读过很多关于内存排序的文章,他们都只说CPU会重新排序加载和存储。 CPU(我对x86 CPU特别感兴趣)是否只对加载和存储进行重新排序,而不对其拥有的其余指令进行重新排序?

  • 我正在尝试排除要嵌入到JAR中的完整文件夹。我发现了以下指令,它就像一个魅力: 因此,在运行时,没问题,我得到了一个没有不需要的文件夹的JAR。 然而,我有几个项目需要包含此指令(以及其他常见配置),因此我使用的是父POM项目。除上述指令外,一切正常。一旦我将这个排除部分移到父POM定义中,它就不再工作了。 奇怪的是,如果我比较两种配置的有效POM,它们是完全相同的! 在当前项目的POM或父POM

  • 本文向大家介绍v-show指令算是重排吗?相关面试题,主要包含被问及v-show指令算是重排吗?时的应答技巧和注意事项,需要的朋友参考一下 当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建,因v-show指令改变display的属性,会发生重排。