看《Java並發編程實戰》遇到如下問題
代碼:
/**
* Created by yesiming on 16/11/11.
*/
public class Holder {
private int n;
public Holder(int n) {
this.n = n;
}
public void assertSanity() {
if(n != n) {
throw new AssertionError("This statment is false.");
}
}
}
疑問是:assertSanity() 方法中的判斷 n != n
n它怎麼就能不等於n呢,它們是同一個變量呀
解惑:設想一下場景
有2個線程 A,B
A做的操作:Holder holder = new Holder(42);
B做的操作:
if(holder != null) {
holder.assertSantiy();
}
對於線程A的操作,jvm執行時的步驟:1.棧裡生成holder引用,2.執行構造函數,在堆裡生成Holder的內存空間,並且給n賦值為42,3.把holder指向堆裡生成的內存空間
問題是:上面的1,2,3步驟不是按照1,2,3的順序執行的,執行引擎對指令重排序後,可能會按照1,3,2的順序執行,也可能是別的順序
結果:這樣就導致當holder指向了堆裡的內存空間時(這時holder不是null了),但是構造函數執行尚未完成,n還沒有被賦值為42。
對於線程B的操作,assertSanity()方法編譯後的指令如下:
public void assertSanity();
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field n:I
4: aload_0
5: getfield #2 // Field n:I
8: if_icmpeq 21
11: new #3 // class java/lang/AssertionError
14: dup
15: ldc #4 // String This statment is false.
17: invokespecial #5 // Method java/lang/AssertionError."<init>":(Ljava/lang/Object;)V
20: athrow
21: return
LineNumberTable:
line 14: 0
line 15: 11
line 17: 21
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lo1/Holder;
}
注意看綠色部分的指令,
第一步:1: getfield #2 // Field n:I
取得n的值
第二部:5: getfield #2 // Field n:I
取得n的值
也就是說,在執行比較指令if_icmpeq之前,要讓比較的兩個數都進棧,然後做比較
那麼,既然要進棧2次,也就可以推導出,當線程B操作第一步getField時,n沒有被線程A賦值,那麼這個n是0,之後,線程A修改了n的值,第二次進棧時,n的值已經是修改後的42了
這樣,就會導致棧頂2個slot中的數,一個0,一個是42,必然會導致8: if_icmpeq的結果為真
解決方案:
把n定義成final,並且在聲明Holder時,使用valetile關鍵字
final能保證Holder在構造方法執行時,不會被執行引擎重排序,也就不會出現holder指向了Holder構造產生的內存空間,但是構造方法沒有執行完成的情況(n沒有被賦值)
valetile保證了在外部程序中,線程A和線程B對Holder的更新狀態是隨時可見的