局部变量保证线程安全

336 阅读3分钟

局部变量保证线程安全

首先来看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);              /* 代码③ */
}

hashString类的一个属性,可以看到这边首先是代码①读取了本地属性的值,并且赋值给局部变量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 。