局部變量保證線程安全
首先來看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
進行了業(yè)務(wù)邏輯的判斷朴沿。如果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); /* 代碼② */
}
修改的代碼沒有局部變量沦零,直接使用屬性本身來操作。
答案是否定的货岭,因為這種寫法是線程不安全的路操,可能導(dǎo)致方法的返回值是 0 疾渴。似乎有點費解,因為如果hash
值為0 屯仗,則代碼會進入循環(huán)體搞坝,對hash
值進行更新。所以乍看之下魁袜,無論如何是不會返回 0 的桩撮。
上述的理解邏輯,在單線程環(huán)境下峰弹,是正確的店量。但是這段代碼工作在多線程環(huán)境。實際上鞠呈,上述代碼有兩次對hash
值的讀取融师,分別是代碼①和②∫狭撸可能會出現(xiàn)一種情況旱爆,在代碼①處,讀取到hash
值不為 0 窘茁,在代碼②處怀伦,讀取到hash
值為0,并且以此為結(jié)果返回了庙曙。顯然此時這種結(jié)果是錯誤的空镜。
要理解這種場景的發(fā)生需要從 JMM 的規(guī)則談起。首先捌朴,兩個讀取之間是沒有因果關(guān)系的吴攒,因此不存在第一個對變量的讀取觀察到了值,第二個對該變量的讀取也要觀察到這個值砂蔽。其次洼怔,在 JMM 中,對一個變量的讀取操作允許其觀察最后一次到對該變量的寫入左驾,只要沒有 HB 關(guān)系來阻止這個讀取的觀察效果镣隶。此外,對象屬性的默認值也是由寫入動作觸發(fā)的诡右。這意味著對hash
值的寫入有兩個地方安岂,一個在于對象構(gòu)造時,一個在于其他線程對hash
值的寫入帆吻。由于這兩個寫入沒有 HB 關(guān)系域那,因此對hash
的讀取可能讀取到任意一個寫入的結(jié)果。所以猜煮,可能會出現(xiàn)的情況是在代碼①處讀取到了其他線程對hash
值的寫入次员,因此跳過了內(nèi)部的寫入邏輯败许。而在代碼②處再次讀取hash
值,此時讀取到了對象構(gòu)造時對hash
默認值的寫入淑蔚,導(dǎo)致返回 0 市殷。
從 JMM 規(guī)則角度是最正確的理解,但是為了形象的想象這一切如何發(fā)生刹衫,我們可以將上面的程序修改如下
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); /* 代碼② */
}
實際上醋寝,這的確是在執(zhí)行代碼邏輯的時候,一種可能的代碼重排序變種带迟。假定一開始hash
值為0甥桂,則a
為 0 。在if
判斷的時候邮旷,hash
讀取到了其他線程寫入的值,因此沒有執(zhí)行計算邏輯蝇摸,最終返回了a
的值婶肩,也就是 0 。