2011-11-30 75 views
6

我有一個單一實現的公共接口。接口定義了可以拋出檢查異常的初始化方法。單體對象的工廠:這個代碼是線程安全的嗎?

我需要一個工廠,它會按需要返回緩存的單例實現,並想知道下面的方法是否是線程安全的?

UPDATE1:請不要提出任何第三部分庫,因爲這將需要獲得法律間隙由於可能的許可問題:-)

UPDATE2:此代碼將很有可能在使用EJB環境,所以最好不要產生額外的線程或使用類似的東西。

interface Singleton 
{ 
    void init() throws SingletonException; 
} 

public class SingletonFactory 
{ 
    private static ConcurrentMap<String, AtomicReference<? extends Singleton>> CACHE = 
     new ConcurrentHashMap<String, AtomicReference<? extends Singleton>>(); 

    public static <T extends Singleton> T getSingletonInstance(Class<T> clazz) 
     throws SingletonException 
    { 
     String key = clazz.getName(); 
     if (CACHE.containsKey(key)) 
     { 
      return readEventually(key); 
     } 

     AtomicReference<T> ref = new AtomicReference<T>(null); 
     if (CACHE.putIfAbsent(key, ref) == null) 
     { 
      try 
      { 
       T instance = clazz.newInstance(); 
       instance.init(); 
       ref.set(instance); // ----- (1) ----- 
       return instance; 
      } 
      catch (Exception e) 
      { 
       throw new SingletonException(e); 
      } 
     } 

     return readEventually(key); 
    } 

    @SuppressWarnings("unchecked") 
    private static <T extends Singleton> T readEventually(String key) 
    { 
     T instance = null; 
     AtomicReference<T> ref = (AtomicReference<T>) CACHE.get(key); 
     do 
     { 
      instance = ref.get(); // ----- (2) ----- 
     } 
     while (instance == null); 
     return instance; 
    } 
} 

我對線條(1)和(2)不完全確定。我知道被引用的對象在AtomicReference中被聲明爲易失性字段,因此在第(1)行所做的更改應該立即在第(2)行中可見 - 但仍然存在一些疑問... ...

除此之外 - 我認爲使用ConcurrentHashMap解決了將新密鑰放入緩存的原子性。

你們看到這種方法的任何擔憂?謝謝!

PS:我知道靜態holder類成語 - 我不使用它由於ExceptionInInitializerError(其中單實例化過程中引發的任何異常被包裝成)和隨後的NoClassDefFoundError這是不是我要趕。相反,我想利用專用檢查異常的優勢,通過捕獲並優雅地處理它,而不是解析EIIR或NCDFE的堆棧跟蹤。

回答

0

考慮使用番石榴的CacheBuilder。例如:

private static Cache<Class<? extends Singleton>, Singleton> singletons = CacheBuilder.newBuilder() 
    .build(
     new CacheLoader<Class<? extends Singleton>, Singleton>() { 
     public Singleton load(Class<? extends Singleton> key) throws SingletonException { 
      try { 
      Singleton singleton = key.newInstance(); 
      singleton.init(); 
      return singleton; 
      } 
      catch (SingletonException se) { 
      throw se; 
      } 
      catch (Exception e) { 
      throw new SingletonException(e); 
      } 
     } 
     }); 

public static <T extends Singleton> T getSingletonInstance(Class<T> clazz) { 
    return (T)singletons.get(clazz); 
} 

注意:此示例未經測試且未編譯。

番石榴的底層Cache實現將爲您處理所有緩存和併發邏輯。

+0

謝謝!在我的情況下,第三方lib不是一個選項... – anenvyguest

-1

代碼通常不是線程安全的,因爲CACHE.containsKey(key)檢查和CACHE.putIfAbsent(key, ref)調用之間存在差距。兩個線程可以同時調用該方法(特別是在多核/處理器系統上)並執行containsKey()檢查,然後都嘗試執行放置和創建操作。

我會保護getSingletonInstnace()方法的執行,使用鎖或通過在某種顯示器上同步。

+1

他通過將'AtomicReference'與'null'值進行對比來防止這種情況。如果'putIfAbsent()'不返回null,它就會放棄它並調用get。這就是循環的重點。 – Gray

3

具有所有這些併發/原子事情會導致更多的鎖定問題,不僅僅是將圍繞吸氣

synchronized(clazz){} 

塊。原子參考是用於已更新的參考,並且您不想碰撞。在這裏你有一個作家,所以你不關心這一點。

您可以通過有一個HashMap進一步優化它,只有當存在一個缺失,使用synchronized塊:

public static <T> T get(Class<T> cls){ 
    // No lock try 
    T ref = cache.get(cls); 
    if(ref != null){ 
     return ref; 
    } 
    // Miss, so use create lock 
    synchronized(cls){ // singletons are double created 
     synchronized(cache){ // Prevent table rebuild/transfer contentions -- RARE 
      // Double check create if lock backed up 
      ref = cache.get(cls); 
      if(ref == null){ 
       ref = cls.newInstance(); 
       cache.put(cls,ref); 
      } 
      return ref; 
     } 
    } 
} 
+0

謝謝!我正在考慮這種方法,但是在「Java併發實踐」一書中有一個推理,中等負載下的基於原子的算法優於那些基於鎖類的算法,後者又優於基於鎖的內部算法。不過,我傾向於你提供的代碼,因爲它更具可讀性:-) – anenvyguest

+0

是的,但作爲一個單身人士,你永遠不會遇到LOAD,因爲創建是唯一被鎖定的部分。獲得者全部不同步。而且根據我的經驗,旋轉一直比封鎖更糟。 – Nthalk

+0

除了我以前的評論 - 我認爲普通的java.util.HashMap在這裏是不夠的,因爲'cache.put(cls,ref)'可以觸發重建內部表,因此'cache.get(cls)'可以看到地圖處於不一致的狀態,因爲後面的調用是由'synchronized'塊執行的。 – anenvyguest

0

這看起來像它的工作,雖然我可能會考慮某種形式的睡眠,如果連的測試時需要設置的參考值爲納秒。旋轉測試循環將非常昂貴。

而且,我會考慮通過將AtomicReferencereadEventually()這樣就可避免containsKey()然後putIfAbsent()競爭條件改善的代碼。所以代碼將是:

AtomicReference<T> ref = (AtomicReference<T>) CACHE.get(key); 
if (ref != null) { 
    return readEventually(ref); 
} 

AtomicReference<T> newRef = new AtomicReference<T>(null); 
AtomicReference<T> oldRef = CACHE.putIfAbsent(key, newRef); 
if (oldRef != null) { 
    return readEventually(oldRef); 
} 
... 
3

你已經去了很多工作,以避免同步,我假設這樣做的原因是出於性能考慮。你有沒有測試過,看看這是否實際上提高了性能與同步解決方案?

我問的原因是Concurrent類傾向於比非併發類更慢,更不用說使用原子引用的額外重定向級別。根據您的線程爭用情況,一個天真的同步解決方案實際上可能會更快(並且更容易驗證正確性)。

此外,我認爲當調用instance.init()期間引發SingletonException時,可能會導致無限循環。原因是在readEventually中等待的併發線程永遠不會找到它的實例(因爲在另一個線程初始化實例時拋出異常)。也許這對你的情況來說是正確的行爲,或者你想爲實例設置一些特殊的值來觸發拋出等待線程的異常。

+0

好趕上!當拋出異常時,我實際上錯過了無限循環。 – anenvyguest

-1

google「Memoizer」。基本上,而不是AtomicReference,使用Future