局部变量保证线程安全
首先来看String
这个类的hashcode
方法,如下
public int hashCode()
{
int h = hash; /* 代码① */
if ( h == 0 && value.length > 0 )
{
char val[] = value;
for ( int i = 0; i < value.length; i++ )
{
h = 31 * h + val[i];
}
hash = h; /* 代码② */
}
return(h); /* 代码③ */
}
hash
是String
类的一个属性,可以看到这边首先是代码①读取了本地属性的值,并且赋值给局部变量h
。并且使用h
进行了业务逻辑的判断。如果h
没有值的话,就进行 Hash 值的生成,并且赋值到h
上,并且在代码②处赋值给了属性hash
。最终返回的,也是局部变量h
的值。那么上述的代码能否修改为下面的模式
public int hashCode()
{
if ( hash == 0 && value.length > 0 ) /* 代码① */
{
char val[] = value;
int h = 0;
for ( int i = 0; i < value.length; i++ )
{
h = 31 * h + val[i];
}
hash = h;
}
return(hash); /* 代码② */
}
修改的代码没有局部变量,直接使用属性本身来操作。
答案是否定的,因为这种写法是线程不安全的,可能导致方法的返回值是 0 。似乎有点费解,因为如果hash
值为0 ,则代码会进入循环体,对hash
值进行更新。所以乍看之下,无论如何是不会返回 0 的。
上述的理解逻辑,在单线程环境下,是正确的。但是这段代码工作在多线程环境。实际上,上述代码有两次对hash
值的读取,分别是代码①和②。可能会出现一种情况,在代码①处,读取到hash
值不为 0 ,在代码②处,读取到hash
值为0,并且以此为结果返回了。显然此时这种结果是错误的。
要理解这种场景的发生需要从 JMM 的规则谈起。首先,两个读取之间是没有因果关系的,因此不存在第一个对变量的读取观察到了值,第二个对该变量的读取也要观察到这个值。其次,在 JMM 中,对一个变量的读取操作允许其观察最后一次到对该变量的写入,只要没有 HB 关系来阻止这个读取的观察效果。此外,对象属性的默认值也是由写入动作触发的。这意味着对hash
值的写入有两个地方,一个在于对象构造时,一个在于其他线程对hash
值的写入。由于这两个写入没有 HB 关系,因此对hash
的读取可能读取到任意一个写入的结果。所以,可能会出现的情况是在代码①处读取到了其他线程对hash
值的写入,因此跳过了内部的写入逻辑。而在代码②处再次读取hash
值,此时读取到了对象构造时对hash
默认值的写入,导致返回 0 。
从 JMM 规则角度是最正确的理解,但是为了形象的想象这一切如何发生,我们可以将上面的程序修改如下
public int hashCode()
{
int a = hash;
if ( hash == 0 && value.length > 0 ) /* 代码① */
{
char val[] = value;
int h = 0;
for ( int i = 0; i < value.length; i++ )
{
h = 31 * h + val[i];
}
a = hash = h;
}
return(a); /* 代码② */
}
实际上,这的确是在执行代码逻辑的时候,一种可能的代码重排序变种。假定一开始hash
值为0,则a
为 0 。在if
判断的时候,hash
读取到了其他线程写入的值,因此没有执行计算逻辑,最终返回了a
的值,也就是 0 。