Singleton 單例模式

動機

有些情況下,一個類只能有一個實例是很重要的。比如說,在操作系統(tǒng)中只能有一個窗口管理器的(文件系統(tǒng)或打印機程序)。通常, 單實例用于對內(nèi)部或外部資源的集中式管理,同時它們提供一個訪問其自身的全局入口。

單例模式是最簡單的設(shè)計模式之一。它只涉及到一個負責(zé)實例化它自己的類,這個類保證其只創(chuàng)建一個實例(私有化構(gòu)造函數(shù));同時該類提供一個訪問該實例的全局入口。這樣,程序各處都使用同一實例,不會每次都直接調(diào)用構(gòu)造函數(shù)。

目的

  • 確保一個類只創(chuàng)建一個實例
  • 提供訪問該單一實例的全局入口

實現(xiàn)

具體實現(xiàn)涉及 Singleton 類的一個靜態(tài)私有成員,一個私有構(gòu)造函數(shù)和一個共有方法返回該靜態(tài)私有成員的引用。

單例實現(xiàn) uml

單例模式定義一個 getInstance 方法來暴露供客戶端訪問的單一實例。getInstance() 負責(zé)在單一實例還沒被創(chuàng)建的時候創(chuàng)建它并返回該實例。

class Singleton{
    private static Singleton instance;
    private Singleton(){
        // ...
    }

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

    public void doSomething(){
        //...
    }
}

你可以發(fā)現(xiàn)上面的代碼 getInstance 方法確保只創(chuàng)建一個類實例。不能從類的外部訪問構(gòu)造函數(shù)以保證只能通過 getIntance 方法來創(chuàng)建類實例。

getInstance 方法也作為對象唯一的全局訪問入口,可以像下面這樣使用:

Singleton.getInstance().doSomething();

適用場景 & 例子

根據(jù)定義,單例模式的使用場景應(yīng)該是一個類必須只有一個實例,并且必須從一個全局入口訪問這個實例。以下幾個是使用單例模式的真實案例:

  • 日志類 Logger Classes
    單例模式被用于日志類的設(shè)計中。 這些日志類通常以單例來實現(xiàn),并且在應(yīng)用各組件中提供一個全局的日志記錄入口,執(zhí)行日志記錄操作時就不用每次都創(chuàng)建對象了。
  • 配置類 Configuration Classes
    使用單例模式設(shè)計為應(yīng)用提供配置的類。通過將配置類實現(xiàn)為單例,不單單提供全局訪問入口,我們還可以將這個實例作為緩存對象。當(dāng)實例化類的時候(讀取值),單例會將值保持在其內(nèi)部結(jié)構(gòu)中。如果配置是從數(shù)據(jù)庫或者文件中讀取,這樣就不用每次使用配置參數(shù)時都要去重新載入值了。
  • 共享地訪問資源
    單例模式可以用于設(shè)計需要串行運行的應(yīng)用。假設(shè)應(yīng)用中有許多在多線程環(huán)境中運行的類,這些類需要串行地執(zhí)行操作。在這種情況下, 帶有 synchronized 方法的單例實例就可以用來管理這些串行操作。
  • 單例實現(xiàn)的工廠
    假設(shè)我們設(shè)計一個執(zhí)行于多線程環(huán)境下的應(yīng)用,其中有一個用于生成帶有id的新對象(賬戶,客戶,網(wǎng)站,地址等對象)。如果這工廠類在2個不同的線程中實例化2次,那么就可能出現(xiàn)id重疊的2個不同對象。如果我們將這個 Factory 實現(xiàn)為一個單例就可以避免這個問題。通常將 抽象工廠工廠方法單例模式 一起使用。

特定的問題和實現(xiàn)

為了在多線程下使用,線程安全的實現(xiàn)

一個健壯的單例實現(xiàn)應(yīng)該在任何情況下都能正常工作。這就是為什么我們要確保多線程使用時它也能正常工作的原因。如前面例子的單例確保讀寫操作都是同步的,它可用于多線程應(yīng)用中。

一、 使用雙重鎖定機制實現(xiàn)延遲初始化(懶漢)

上面代碼中展示的標準實現(xiàn)是一種線程安全的實現(xiàn),但它不是最好的線程安全實現(xiàn),因為當(dāng)我們考慮性能時,同步操作的開銷比較大。我們能看出同步的 getInstance 在實例已經(jīng)創(chuàng)建后并不需要再進行同步。如果我們發(fā)現(xiàn)實例已經(jīng)創(chuàng)建,我們只需返回這個實例,而不需要使用任何同步代碼塊。這個優(yōu)化在于在非同步代碼塊中檢查實例是否為 null, 再在同步代碼塊中檢驗是否 null 并且創(chuàng)建實例。這稱為雙重鎖定機制。

在這種情況下,單例實例在第一次調(diào)用 getInstance() 方法的時候創(chuàng)建。這就叫延遲初始化,并且它確保這個單例的實例只在需要的時候創(chuàng)建。

// 使用雙重鎖定機制的延遲初始化
class Singleton{
    private static volatile Singleton instance; 

    private Singleton(){}

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

    public void doSomething(){
         // ...
    }
}

關(guān)于為什么要加 volatile 可以參考下 https://www.ibm.com/developerworks/cn/java/j-jtp06197.html

二、 使用靜態(tài)字段實現(xiàn)預(yù)先加載 (餓漢)

由于以下實現(xiàn)中單例實例被聲明為靜態(tài)成員了,在類加載的時候就實例化了而不是第一次使用它的時候。這就是為什么我們不再需要同步代碼了。 類只加載一次保證實例的唯一性。

class Singleton{
    private static Singleton instance = new Singleton();

    private Singleton() {
         //...
    }

    public static Singleton getInstance(){
        return instance;
    }

    public void doSomething(){
        //...
    }
}

protected constructor

可以使用 protected 訪問修飾符的構(gòu)造函數(shù)來授權(quán)給子類。但是這種技術(shù)有2個缺陷,使得單例的繼承不切實際:

  • 首先,如果構(gòu)造函數(shù)是 protected, 意味著這個類可以被同一個包內(nèi)的其他類實例化??赡艿拇胧┦歉綦x單例類。
  • 其次,要使用派生類,所有的 getInstance 調(diào)用都得從現(xiàn)有代碼中的 Singleton.getInstance() 改為 NewSingleton.getInstance()

如果多個 classloader 訪問同一個單例類,會有多個單例實例

如果一個類(相同類名,相同包名)被2個不同的 classloader 加載,那么他們代表內(nèi)存中2個不同的類。

序列化

如果單例類實現(xiàn)了 java.io.Serializable 接口,當(dāng)單例實例被序列化和反序列化多次時,就會創(chuàng)建多個單例類實例。為了避免這種情況,必須實現(xiàn) readResolve 方法。 參考下 Serializable () 和 readResolve 方法的說明。

抽象工廠工廠方法 實現(xiàn)為單例

在一些特定的場景下工廠必須是唯一的。存在2個工廠的話,創(chuàng)建對象時會有意料之外的影響。為了確保工廠的唯一性,它要實現(xiàn)成單例。這樣做之后我們也避免了使用前的工廠實例化。

Hot Spot:

  • 多線程: 當(dāng)單例運行于多線程應(yīng)用時,必須格外小心
  • 序列化: 當(dāng)單例類實現(xiàn)了 Serializable 接口,它們必須實現(xiàn) readResolve 方法以避免 2 個不同的對象
  • Classloaders 如果單例類被2個不同的類加載器加載,我們將得到2個不同類,一個類加載器一個
  • 由類名表示的全局訪問入口:單例類的實例通過類名來獲取。乍一看,這樣很容易訪問實例,但這不是很靈活。如果我們要替換這個單例類,就要修改代碼中所有的引用。

jdk 中的使用

**
 * Every Java application has a single instance of class
 * <code>Runtime</code> that allows the application to interface with
 * the environment in which the application is running. The current
 * runtime can be obtained from the <code>getRuntime</code> method.
 * <p>
 * An application cannot create its own instance of this class.
 *
 * @author  unascribed
 * @see     java.lang.Runtime#getRuntime()
 * @since   JDK1.0
 */
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

    // ...
}

more
示例代碼:https://github.com/minorpoet/design-patterns/tree/master/Singleton
classloader: http://ifeve.com/classloader/
volatile: http://www.itdecent.cn/p/3893fb35240f
double-check-lock is broken: http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容