2010-03-24 72 views
5

我使用joda,因爲它在多線程方面的聲譽很好。通過使所有Date/Time/DateTime對象不可變來實現多線程日期處理的效率很高。部分構造的對象/多線程

但是,我不確定喬達是否真的在做正確的事情。它可能會,但我很感興趣看到一個解釋。

當一個DateTime的一個toString()被調用喬達執行以下操作:

/* org.joda.time.base.AbstractInstant */ 
public String toString() { 
    return ISODateTimeFormat.dateTime().print(this); 
} 

所有格式化是線程安全的(他們永恆不變的爲好),但什麼是關於格式工廠:

private static DateTimeFormatter dt; 

/* org.joda.time.format.ISODateTimeFormat */ 
public static DateTimeFormatter dateTime() { 
    if (dt == null) { 
     dt = new DateTimeFormatterBuilder() 
      .append(date()) 
      .append(tTime()) 
      .toFormatter(); 
    } 
    return dt; 
} 

這是單線程應用程序中的一種常見模式,但已知它在多線程環境中容易出錯。

我看到以下的危險:空檢查期間

  • 競爭條件 - >最壞的情況:獲得創建兩個對象。

沒有問題,因爲這只是一個輔助對象(與正常的單例模式不同),一個會保存在dt中,另一個會丟失,並且遲早會被垃圾回收。

  • 的objec已完成之前初始化

靜態變量可能指向一個部分構造的對象(叫我瘋狂之前,閱讀有關本Wikipedia article類似的情況。)

所以Joda如何確保部分創建的格式化程序不會在此靜態變量中發佈?

感謝您的解釋!

雷託

回答

4

你說,格式化程序是隻讀的。如果他們只使用最終字段(我沒有閱讀格式化程序源),那麼在Java語言規範的第3版中,他們就會被「最終字段語義」的部分對象所保護。我沒有檢查第2版JSL版本,也不確定,如果這種初始化在該版本中是正確的。

請參閱JLS第17.5章和第17.5.1章。我將爲所需要的事情建立一個「事件鏈」。

首先,在構造函數的某個地方寫入格式化程序的最後一個字段。這是寫w。當構造函數完成時,一個「凍結」操作就開始了。我們稱之爲f。在程序順序的某個地方(在從構造函數返回後,也許有其他一些方法並從toFormatter返回)之後,會寫入dt字段。讓我們給這個寫一個名字a。這個寫(a)在「程序順序」(在單線程執行中的順序)的凍結動作(f)之後,因此f發生在僅由JLS定義的(hb(f,a))之前。 P,初始化完成... :)

有時,在另一個線程中,會調用dateTime()。格式。那時我們需要兩次讀取。首先讀取格式化程序對象中的最終變量。我們稱之爲r2(與JLS保持一致)。其中的第二個是Formatter的「this」的讀取。這發生在讀取dt字段時調用dateTime()方法期間。我們稱之爲r1。我們現在有什麼?閱讀r1看到一些寫給dt。我認爲這個寫作是上一段的行動(只有一個線程寫了這個字段,只是爲了簡單)。由於r1參見寫a,則存在mc(a,r1)(「內存鏈」關係,第一個子句定義)。當前線程沒有初始化格式化程序,在操作r2中讀取它的字段,並在操作r1處看到格式化程序的「地址」。因此,根據定義,有一個解引用(r1,r2)(從JLS排序的另一個動作)。

我們在凍結前寫過hb(w,f)。我們在dt,hb(f,a)的賦值之前凍結。我們從dt,mc(a,r1)讀取。我們在r1和r2之間有一個解引用鏈(dereferences)(r1,r2)。所有這一切都只是通過JLS定義而導致了一個發生之前的關係hb(w,r2)。此外,根據定義,hb(d,w)其中d是寫入對象中最後一個字段的默認值。因此,讀取r2不能看到寫入w,並且必須看到寫入r2(唯一從程序代碼寫入該字段)。

相同的是更多間接字段訪問的順序(存儲在最終字段中的對象的最終字段等)。

但這不是全部!沒有訪問偏好構造的對象。但有一個更有趣的錯誤。缺少任何顯式的synhronization dateTime()可能返回null。我不認爲這種行爲在實踐中可以觀察到,但是JLS第3版不能阻止這種行爲。方法中的第一次讀取dt字段可能會看到一個由另一個線程初始化的值,但dt的第二次讀取可以看到「寫入defalut值」。沒有發生 - 在關係存在以防止它。這種可能的行爲是特定於第三版的,第二版具有「寫入主存儲器」/「從主存儲器讀取」,這不允許線程看到時間倒數的值。

+0

+1。這是java併發模型中爲什麼不可變對象更「簡單」的另一個原因。但是,我不確定最後一段,請參閱JLS 17.4.4:「向每個變量寫入默認值(零,false或null) - 與每個線程中的第一個操作同步」。在讀取dt之前發生「寫入默認值」是必要的。 – irreputable 2010-03-24 20:45:43

+0

是的,在讀取dt之前寫入默認值。這可以防止線程讀取「垃圾」值。但是在另一個線程中寫入字段(初始化dt字段)不與dt讀取具有hb關係。因此,我們可以隨時看到這兩個寫入中的任何一個(默認或來自初始化dt的線程)。我們必須讀取默認初始值的唯一時間是臨時性需求檢查。但是那時我們只提交一個默認值的讀取,並且在下一次迭代中說,該線程將一個對象寫入dt(僅用於該方法中的第一次讀取)。 – maxkar 2010-03-25 06:14:33

-1

IMO最壞的情況是越來越創建不是兩個對象,但幾個(多達有線程調用dateTime(),要準確)。由於dateTime()未同步,且dt既不是最終的也不是易失性的,因此不保證其在一個線程中的值更改對其他線程可見。因此,即使在一個線程初始化爲dt之後,其他任何線程仍然可以將引用視爲null,因此可以愉快地創建新對象。

除此之外,正如其他人所解釋的,部分創建的對象無法通過dateTime()發佈。由於參考值更新保證是原子性的,因此也不能部分更改(=懸掛)參考。

0

這是一個有點不回答,但對於

最簡單的解釋那麼,如何喬達確保不部分創建的格式被髮表在靜態變量?

可能只是因爲他們沒有確定任何東西,開發人員沒有意識到它可能是一個bug或者認爲它不值得同步。

0

I asked a similar question在2007年Joda郵件列表中,雖然我沒有找到答案是確鑿的,我避免了喬達時間,因此無論好壞。

Java語言規範的第3版保證對象引用更新是原子的,無論它們是32位還是64位。這與上面提到的論點相結合,使得Joda代碼線程安全的IMO(參見java.sun.com/docs/books/jls/third_edition/html/memory.html#17.7)

IIRC,版本2 of JLS沒有包含關於對象引用的明確說明,也就是說只有32位ref是原子保證的,所以如果你使用的是64位JVM,則不能保證它可以工作。當時我正在使用JLS v3之前的Java 1.4。