小编典典
在修改后的代码中:
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。
2020-09-24