2011-07-07 44 views
9

這裏最近的一個問題有下面的代碼(很好,類似於這個)來實現沒有同步的單例。Java是否有靜態命令初始化失敗?

public class Singleton { 
    private Singleton() {} 
    private static class SingletonHolder { 
     private static final Singleton INSTANCE = new Singleton(); 
    } 
    public static Singleton getInstance() { 
     return SingletonHolder.INSTANCE; 
    } 
} 

現在,我想明白這是做什麼。由於實例是static final,因此它在任何線程調用getInstance()之前就已經建好了,因此不需要同步。

僅當兩個線程同時嘗試呼叫getInstance()(並且該方法在第一次呼叫而不是在"static final"時間進行構建時)才需要同步。因此

我的問題基本上是:爲什麼那麼你會永遠喜歡單身的懶建設的東西,如:

public class Singleton { 
    private Singleton() {} 
    private static Singleton instance = null; 
    public static synchronized Singleton getInstance() { 
     if (instance == null) 
      instance = new Singleton(); 
     return instance; 
    } 
} 

我唯一的想法是,使用static final方法可能引入排序問題,因爲在C++中的靜態初始化命令失敗。

首先,它的Java實際上這個問題?我知道訂單內一類是完全指定,但它在某種程度上保證一致的順序類之間(如用類加載器)?其次,如果訂單一致,那麼爲什麼懶惰施工選擇有利呢?

回答

12

現在,我想明白這是做什麼。由於實例是靜態最終的,因此它在任何線程都會調用getInstance()之前就已經建好了,所以沒有實際的同步需求。

不太。它建立在SingletonHolder類別爲initialized時發生,第一次調用getInstance。類加載器具有單獨的鎖定機制,但是在加載類之後,不需要進一步的鎖定,因此該方案只進行了足夠的鎖定以防止多個實例化。

首先,Java真的有這個問題嗎?我知道類中的順序是完全指定的,但它是否能以某種方式保證類之間的一致順序(比如使用類加載器)?

Java確實存在一個問題,即類初始化循環可能導致某些類在其初始化之前(技術上在所有靜態初始化塊運行之前)觀察到另一個類的靜態最終結果。

考慮

class A { 
    static final int X = B.Y; 
    // Call to Math.min defeats constant inlining 
    static final int Y = Math.min(42, 43); 
} 

class B { 
    static final int X = A.Y; 
    static final int Y = Math.min(42, 43); 
} 

public class C { 
    public static void main(String[] argv) { 
    System.err.println("A.X=" + A.X + ", A.Y=" + A.Y); 
    System.err.println("B.X=" + B.X + ", B.Y=" + B.Y); 
    } 
} 

工作C打印

A.X=42, A.Y=42 
B.X=0, B.Y=42 

但是在您發佈的成語,有助手和單間沒有周期,所以沒有理由,更喜歡偷懶的初始化。

+0

我認爲延遲初始化的普遍原因是推遲某些東西(可能)昂貴,直到絕對需要,希望它根本不需要。假設您的應用程序想要下載比x更長的內容。用戶可以在x之前關閉它,在這種情況下(通常是昂貴的)連接創建將是浪費。 –

+0

@Michal,如果你在OP中執行一個小型輔助類的加載計算,那麼Java的延遲加載可以讓你獲得延遲初始化的所有好處。如果它不是應該成爲全球的東西w.r.t.一個類加載器,然後這不是一個選項。 –

2

現在,我想明白這是 在做什麼。由於實例最終是靜態的,因此在任何線程調用getInstance()之前就已經建好了,因此 沒有真正需要同步。

編號SingletonHolder只有當您第一次調用SingletonHolder.INSTANCE時纔會加載類。 final對象只有在完全構建後纔會對其他線程可見。這種惰性初始化稱爲Initialization on demand holder idiom

0

只是關於第一個實現的一點點說明:這裏有趣的是類初始化用於替代經典的同步。

類初始化的定義非常明確,因爲除非完全初始化(即所有靜態初始化代碼已經運行),否則代碼無法訪問類的任何內容。並且由於已經加載的類可以以大約零開銷被訪問,所以這限制了「同步」開銷到那些需要完成實際檢查的情況(即,「該類是加載/初始化了嗎?」)。

使用類加載機制的一個缺點是它在中斷時很難調試。如果由於某種原因Singleton構造函數拋出異常,那麼第一個調用者到getInstance()將得到該異常(包裝在另一箇中)。

主叫但是會永遠看到問題的根本原因(他只會得到一個NoClassDefFoundError)。因此,如果第一個主叫方以某種方式忽略了這個問題,那麼你會永遠不會能夠找出什麼出錯了。

如果你只使用同步,那麼第二個被調用者只會試圖再次實例化Singleton,並且可能會遇到同樣的問題(甚至成功!)。

0

在第一個版本的代碼是安全懶洋洋地構建一個單身的正確最佳方式。 Java內存模型保證實例會:

  • 只有先當實際使用(即懶惰),因爲類被加載,只有當第一次使用
  • 是構建恰好一次所以這是完全線程安全的初始化,因爲所有的靜態初始化都保證在類可用之前完成

版本1是一個很好的模式。

EDITED
第2版是線程安全的,但有點貴,更重要的是,嚴重限制了併發性/吞吐量

+1

他的#2是線程安全的。儘管太貴了。 – irreputable

+1

也許我不明白,但我認爲版本2也是線程安全的:public static synchronized Singleton getInstance()。它只是在每個訪問上進行同步的開銷。 –

+0

對不起,我的壞 - 我錯過了方法同步 - 你是正確的,它是安全的。 +1給你們每個人:) – Bohemian

1

,你(這裏通過SingletonHolder.INSTANCE)中所描述的作品有兩個原因

  1. 類加載和初始化第一次訪問時,百通
  2. 類加載和初始化在Java中是
原子

所以你確實以線程安全和有效的方式執行延遲初始化。對於同步的惰性init,此模式更適合替代雙鎖(不工作)解決方案。

0

一個類在運行時被訪問時被初始化。所以init命令幾乎是執行順序。

這裏的「訪問」是指限制動作specified in the spec。下一節討論初始化。

這是怎麼回事在你的第一個例子是等效

public static Singleton getSingleton() 
{ 
    synchronized(SingletonHolder.class) 
    { 
     if(! inited (SingletonHolder.class)) 
      init(SingletonHolder.class); 
    } 
    return SingletonHolder.INSTANCE; 
} 

(初始化完成後,將同步塊變得無用; JVM將優化其關閉。)

語義上說,這是不是從第二個不同IMPL。這並不能真正超越「雙重檢查鎖定」,因爲它雙重檢查鎖定。

由於它在類初始化語義上捎帶,它只適用於靜態實例。一般來說,懶惰評估不限於靜態實例;想象每個會話都有一個實例。

0

首先,Java真的有這個問題嗎?我知道類中的順序是完全指定的,但它是否能以某種方式保證類之間的一致順序(比如使用類加載器)?

確實如此,但程度較輕比C++:

  • 如果沒有依賴循環,靜態初始化發生在正確的順序。

  • 如果在一組類的靜態初始化中存在依賴性循環,那麼這些類的初始化順序是不確定的。

  • 但是,Java保證靜態字段的默認初始化(爲null/zero/false)發生在任何代碼看到字段的值之前。因此,無論初始化順序如何,類(理論上)都可以寫出來做正確的事情。

其次,如果順序是一致的,爲什麼會有史以來最懶的建築選項是有利的?

延遲初始化是在許多情況下非常有用:

  • 當初始化的副作用,你不希望發生的事情除非的對象實際上將被使用。

  • 當初始化開銷很大時,您不希望浪費時間做這些事情,或者您希望更快速地發生更重要的事情(例如,顯示UI)。

  • 當初始化取決於某些在靜態初始化時不可用的狀態。 (雖然你必須小心,因爲當延遲初始化被觸發兩種狀態可能無法使用。)

您還可以實現利用同步getInstance()方法延遲初始化。這很容易理解,但它使得getInstance()分數變慢。

0

我不是你的代碼片段,但我有你的問題的答案。是的,Java有一個初始化命令失敗。我遇到了相互依賴的枚舉。示例如下:

enum A { 
    A1(B.B1); 
    private final B b; 
    A(B b) { this.b = b; } 
    B getB() { return b; } 
} 

enum B { 
    B1(A.A1); 
    private final A a; 
    B(A a) { this.a = a; } 
    A getA() { return a; } 
} 

關鍵是在創建實例A.A1時必須存在B.B1。並創建A.A1 B.B1必須存在。

我的現實生活中的用例有點複雜 - 枚舉之間的關係實際上是父子關係,因此一個枚舉返回對其父對象的引用,而是對其子對象的第二個對象。孩子們是枚舉的私有靜態字段。有趣的是,在Windows上開發時一切正常,但在生產中 - 這是Solaris - 子數組的成員爲空。該數組具有適當的大小,但其元素爲空,因爲在數組實例化時它們不可用。

所以我結束了第一次調用的同步初始化。:-)

0

Java中的唯一正確singletone可以聲明不是類,而是由枚舉:

public enum Singleton{ 
    INST; 
    ... all other stuff from the class, including the private constructor 
} 

的用途是作爲:

Singleton reference1ToSingleton=Singleton.INST;  

所有其他方面不排除重複通過反射實例化,或者如果類的來源直接存在於應用程序源中。 Enum不包括的所有內容。 (The final clone method in Enum ensures that enum constants can never be cloned