在修改后的代码中:
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 hashpre_shared() { if (hash == 0) { hash = 1; } return hash;}public int hashpre_local() { int h = hash; if (h == 0) { hash = h = 1; } return h;}我的机器上的Java编译器生成以下字节码:
public int hashpre_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 r2public int hashpre_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上发生。
*这有点捷径,实际上可能发生的情况是编译器可能会这样重写
hashpre_shared:
public int hashpre_shared() { int h = hash; if (hash != 0) return h; return (hash = 1);}该代码在单线程环境中严格等效(它将始终返回与原始方法相同的值),因此允许重新排序。但是,在多线程环境中,很明显,如果
hash前两行之间的另一个线程将其从0更改为1,则此重新排序的方法将错误地返回0。



