單例模式解析

gnng發表於2019-02-26

單例模式解析

​ 單例模式是設計模式中使用最多的模式之一,它是一種物件的建立模式,用於產生一個物件的例項,確保系統中一個類只有一個例項。在Java語言中,單例模式有以下好處:

  • 對於頻繁使用的物件,單例省略了建立物件的時間,對於重量級物件而言,極大減少了系統開銷。
  • 由於new的次數減少,因此減少了系統對記憶體的使用頻次,減輕了GC壓力,縮短了GC停頓時間。

因此對於系統的關鍵元件和頻繁使用的物件,可以設計為單例模式,減少系統的開銷,提高系統效能。

​ 單例模式的參與者很少,只有單例類和使用者,關係表如下:

角色 作用
單例類 提供單例的工廠,返回單例
使用者 獲取並使用單例

​ 類圖如下:

單例模式解析

實現方式一(餓漢):

單例模式的核心在於通過一個介面返回唯一例項化物件,簡單實現如下:

/**
 * 單例模式--餓漢(無法做到延時載入)
 */
public class HungrySingleton {

    private static HungrySingleton instance = new HungrySingleton();

    /**
     * 私有構造方法
     */
    private HungrySingleton(){
        System.out.println("建立了物件");
    }

    /**
     * 對外暴露的獲取唯一例項的介面
     * @return
     */
    public static HungrySingleton getInstance(){
        return instance;
    }

    /**
     * 序列化,反序列化保證單例
     * @return
     */
    private Object readResolve(){
        return instance;
    }
}
複製程式碼

原理:單例類必須私有化構造方法,保證不會在系統其他地方呼叫,其次instance成員變數和getInstance()方法必須是static修飾的。

注意:單例模式的這種實現方式非常簡單,十分可靠,唯一不足是無法做到延時載入。假設單例的建立過程十分緩慢,由於instance的成員變數是由static修飾的,在JVM載入單例類的時候,單例物件就會存在,如果單例類還在系統中扮演別的角色,那麼系統中任何使用單例類的地方都會初始化這個單例物件,而不管是否被用到。

實現方式二(懶漢):

為了解決上述問題,並提高系統在相關函式呼叫的反應速度,就需要加入延時載入機制,懶漢模式。

/**
 * 單例模式--懶漢(效率低,延時載入)
 */
public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {
        System.out.println("LazySingleton is create");
    }

    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            return new LazySingleton();
        }
        return instance;
    }
}
複製程式碼

原理:首先,靜態成員變數instance賦值為null,確保系統啟動時沒有額外負載,其次在getInstance()方法中判斷當前單例instance物件是否存在,存在則返回,不存在建立單例物件。

注意:getInstance()必須是執行緒同步的,否則在多執行緒條件下,當執行緒1新建單例完成賦值前,執行緒2可能判斷instance為null,執行緒2也建立了單例物件,導致多個例項被建立,因此同步關鍵字是必須的。使用上述單例模式的實現方式,雖然實現了延時載入,但是和第一種實現(餓漢)相比,引入了同步關鍵字,因此在多執行緒場景下,載入速度遠遠大於第一種實現方式,影響系統效能。

實現方式三(內部類):

繼續改進,建立內部類:

/**
 * 使用內部類來維護單例的例項,當StaticSingleton被載入時候,內部類並沒有被初始化
 * (instance並沒有被初始化),呼叫getInstance()才會被初始化。
 */
public class StaticSingleton {

    private StaticSingleton(){
        System.out.println("StaticSingleton is create");
    }

    /**
     * 內部類,建立單例物件
     */
    private static class StaticSingleHolder{
        private static StaticSingleton instance = new StaticSingleton();
    }

    public static StaticSingleton getInstance(){
        return StaticSingleHolder.instance;
    }
}
複製程式碼

原理:在這個實現中,使用內部類來維護單例例項,當StaticSingleton被載入的時候,內部類沒有被初始化,可以確保StaticSingleton載入到JVM中,不會初始化單例類,當呼叫getInstance()時才會載入StaticSingleHolder,初始化instance物件,同時由於例項的建立是在類載入時完成的,對執行緒友好,getInstance()不需要使用同步關鍵字。

注意:使用內部類實現的單例,既可以實現延時載入也避免使用同步關鍵字,是比較完善的實現。但是如果通過反射機制強行呼叫私有構造方法,就會生成多個單例。同時序列化和反序列化可能破壞單例(餓漢程式碼readResolve()方法),場景不多見,如果存在,多加註意。

[^《Java效能程式優化 讓你的Java程式更快、更穩定》 葛一鳴]

相關文章