2013-02-07 131 views
7

我正在閱讀線程和鎖的JLS文檔http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5在多線程環境中讀取值

class FinalFieldExample { 
final int x; 
int y; 
static FinalFieldExample f; 

public FinalFieldExample() { 
    x = 3; 
    y = 4; 
} 

static void writer() { 
    f = new FinalFieldExample(); 
} 

static void reader() { 
    if (f != null) { 
     int i = f.x; // guaranteed to see 3 
     int j = f.y; // could see 0 
    } 
} 
} 

我很困惑上面的例子(ex no 17.5-1)在部分提到的如何f.y可以被視爲零。 Reader線程將讀取對象f爲null,在這種情況下,它不會執行任何操作,或者它將通過一些引用讀取對象f。如果對象f有引用,那麼構造函數必須已經完成其執行,即使多個Writer線程正在運行,以便可以將引用分配給f,並且如果構造函數已經執行,那麼fy應該被視爲4.

In什麼情況可以fy = 0是可能的?

感謝

回答

5

在什麼情況下f.y = 0是可能的?

Java的內存模型允許JIT編譯器重新排序非最終領域構造函數初始化。 x字段是最終的,所以它必須由JVM初始化,但y不是最終的。因此FinalFieldExample可能被分配並設置爲static FinalFieldExample f,但y字段的初始化尚未完成。

引用自17。5-1:

因爲寫入方法在對象的構造函數完成後寫入f,讀取方法將保證看到fx的正確初始化值:它將讀取值3.但是,fy不是最終的;讀者的方法因此不能保證看到它的值4。

因爲f.y是不是最終也無法保證它已被設定時間構造完成並static f分配。所以有一個競賽條件創建和reader可能會看到y爲3或0取決於這場比賽。

+2

這裏可能需要注意術語。對於大多數人來說:'編譯器'意味着'javac',並且該編譯器不會對任何東西重新排序。 JIT編譯器Hotspot可以重新排列指令。但即使如此,'編譯器'可能不是全部,因爲硬件(CPU)也可以執行指令重新排序。緩存行爲也可能導致重新排序指令。 –

+0

好點的熱點編譯器@Martin。我編輯了我的答案。這是我的理解,CPU不做指令重新排序。它可能會傳輸指令,導致它們以不同的順序執行,但不能任意重新排序。 – Gray

+1

是的,我認爲緩存行爲(以及可能的註冊使用)更可能像併發問題描述。 *一些*體系結構可以重新排列指令,但除非實際使用它們,否則這不是問題。 :) –

2

如果一個線程寫入到一個變量,另一個線程讀取它,它有可能是第二個線程沒有看到新的值,即使在讀取時間後發生的。例如,如果兩個線程在不同的處理器上執行,並且寫入的值被緩存在本地處理器寄存器中,則可能會發生這種情況。

Java規範使得這種非直觀的行爲可能爲了提高性能,當你閱讀有關「之前發生(如果這是不可能的,那麼處理器不能使用其本地內存)

所以「在Java存儲器模型中的關係,請記住在程序邏輯中發生」物理「之前(不一定是發生)。您需要明確建立兩個線程之間的「發生之前」關係,例如通過使用易變變量進行同步,或者在此情況下使用最終變量。

+0

我同意,但在這種情況下,我不認爲這是可能的,因爲每次寫入發生在y和x它會發生在不同的內存地址,所以我不認爲JIT編譯器會將它保留在寄存器中。即使它在單個mem地址上只發生一次寫入。不過,我懷疑如果默認啓動在這裏扮演任何邪惡 – Rohit

+0

JIT編譯器會執行JLS允許的所有操作,它認爲這可以提高性能。保持寫入寄存器是合乎邏輯的,因爲如果該字段不是最終的,它可能會改變。 – lbalazscs

+0

我想我在格雷的回答和你的編輯後得到了你的觀點。謝謝 ! – Rohit

1

它不僅是指令重新排序。即使對f.y的寫入在f之前,也可能發生。對象和類都是人類的煙霧和鏡子。在CPU級別,它是所有裝載內存位置和存儲內存位置。數據首先進入CPU緩存。讓我們假設在這種情況下,f到達一個緩存行,而f.y到另一個緩存行。寫入器線程執行其他操作來使第一個緩存行(保持f)對其餘的CPU可見。持有f.y的那個不可見(沒有指示CPU這樣做)。內存位置仍然爲0.當讀取器線程運行在不同的CPU上時,它將加載內存位置,因爲沒有任何事情告訴CPU該位置在另一個CPU緩存中有未決更改。這意味着第二個CPU將從內存中加載f和f.y。 f保存最新值,但最新的f.y仍然在緩存中,所以內存位置爲0.通過放置volatile,final等,實際上是告訴編譯器生成代碼,告訴CPU發佈數據。這只是一個例子,還有更多。

0

f.y可以以下面的方式被視爲0

假設2 threads T1和T2正在運行。 T1正在訪問方法writer,而T2正在加入與FinalFieldExample相同對象的方法reader

  1. T1調用方法writer()f.x已經初始化爲3,因爲它被聲明爲final,它使得它編譯時間常量,所以它在類實例創建之前發生的類FinalFieldExample的初始化期間被初始化爲給定值。由於f未被聲明爲volatile,所以編譯器按照以下方式重新編排對象創建步驟的順序:(a)f被聲明爲非空(b)FinalFieldExample的對象被創建(b)f被引用到該對象。但在點(a)由T1執行之後,T1被T2搶佔
  2. T2稱爲方法reader()。它發現f不爲空,因此它在if塊內。 f.x已被初始化爲3,所以i被賦值爲3。但是f.y直到現在被初始化爲默認值0,因爲在上面給出的步驟1中沒有完成對象的構造。因此j被賦值爲0。

所以我們看到,未聲明變量f揮發給出 compiler自由度以這樣的方式,雖然 執行調用constructor內聯優化代碼,共享 變量f共享線程T1T2可能會立即更新 一旦存儲已被分配,但在內聯構造函數 初始化對象。