單例模式

單例模式(Singleton Pattern

意圖

確保一個類只有一個實例,並提供一個全局訪問點。

單例模式的三個要點:1.單例類只有一個實例對象;2.該單例對象必須由單例類自行創建;3.單例類對外提供一個訪問該單例的全局訪問點。

動機

對於系統中的某些類來說,只能有一個對象,例如:線程池、緩存、註冊表的對象等。若製造出多個實例,就會導致許多問題的產生,如:程序行爲異常、資源使用過度、結果不一致等。
如何保證一個對象只能被實例化一次?
利用靜態變量、靜態方法和適當的訪問修飾符,可以實現。而更好的方法是讓單例類自身負責管理它的唯一實例。

適用性

單例常常用來管理共享的資源,例如:Windows 的回收站、操作系統中的文件系統、顯卡的驅動程序對象、打印機的後臺處理服務、應用程序的日誌對象、數據庫的連接池、網站的計數器、Web 應用的配置對象、應用程序中的對話框等常常被設計成單例。

結構


實現

  • 餓漢式單例
public class Singleton {
    // 私有的靜態屬性
    private static Singleton instance = new Singleton();

    // 私有的構造方法
    private Singleton() {
    }

    // 公共的靜態方法,實例的全局訪問點
    public static Singleton getInstance() {
        return instance;
    }

}

類一旦加載就創建一個實例,保證在調用 getInstance 方法之前實例已經存在了。線程不安全問題主要是由於 instance 被實例化多次,採取直接實例化 instance 的方式就不會產生線程不安全問題。但是直接實例化的方式也丟失了延遲實例化帶來的節約資源的好處。

  • 懶漢式單例(非線程安全)
public class Singleton {
    //私有靜態變量 instance 被延遲實例化
    private static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        // 如果多個線程能夠同時進入,將導致實例化多次 instance
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

}

私有靜態變量 instance 被延遲實例化,他好處是,如果沒有用到該類,那麼就不會實例化,從而節約資源。但是這個實現在多線程環境下是不安全的,如果多個線程能夠同時進入 if (instance == null) ,並且此時 instance 爲 null,那麼會有多個線程執行 instance = new Singleton();

  • 雙重校驗鎖機制(線程安全)
public class Singleton {
    // volatile修飾共享變量,保證了不同線程對變量進行操作時的可見性,可以禁止 JVM 的指令重排序
    private static volatile Singleton instance = null;

    private Singleton() {}

    public static Singleton getUniqueInstance() {
        // 第一次檢查,用來避免 instance 已經被實例化也去做同步操作
        if (instance != null) {
            // 不創建
        } else {
            synchronized (Singleton.class) {
            // 第二次檢查,加鎖,確保只有一個線程進行實例化操作
            if (instance == null) {
                instance = new Singleton();
                }
            }
        }	
        return instance;
    }

}

雙重校驗鎖(Double Check Locking,簡稱DCL),資源利用率高,第一次執行getInstance時單例對象才被實例化;但是第一次加載時反應稍慢一些,在高併發環境下也有一定的缺陷,雖然發生的概率很小。DCL雖然在一定程度解決了資源的消耗、多餘的同步和線程安全等問題,但是他還是在某些情況會出現失效的問題,也就是DCL失效。

  • 靜態內部類單例模式(線程安全)
public class Singleton { 
    // 靜態內部類,只在第一次使用時進行類加載
    private static class MySingletonFactory {  
        // 使用final修飾,可以避免變量被重新賦值,JVM也不用去跟蹤該引用是否被更改
        private static final Singleton INSTANCE = new Singleton();
    } 

    private Singleton(){}

    public static Singleton getInstance(){  
        return MySingletonFactory.INSTANCE;  
    }       
} 

靜態內部類,只在第一次使用時進行類加載,也就是說當調用 getInstance() 從而觸發 MySingletonFactory.INSTANCE 從而觸發時 MySingletonFactory 纔會被加載,此時初始化 INSTANCE 實例,且 JVM 能確保 INSTANCE 只被實例化一次。(不僅可以延遲實例化,而且JVM 提供了對線程安全的支持)。

  • 序列化與反序列化實現單例模式(線程安全)
public class Singleton implements Serializable {

    private static final long serialVersionUID = 1L;

    private static class MySingletonFactory {
        private static final SerializeSingleton instance = new Singleton();
    }

    private Singleton() {}

    public static SerializeSingleton getInstance() {
        return MySingletonFactory.instance;
    }
    // 不加readResolve(),默認的方式運行得到的結果就是多例的
    protected Object readResolve() throws ObjectStreamException {
        System.out.println("調用了readResolve方法!");
            return MySingletonFactory.instance;
    }
}

// 測試僞代碼
public class Test{  
    public static void main(String[] args) throws {  
        Singleton singleton = Singleton.getInstance();    
        File file = new File("MySingleton.txt");    
        // 序列化
        FileOutputStream fos = new FileOutputStream(file); 
        ObjectOutputStream oos = new ObjectOutputStream(fos);  
        oos.writeObject(singleton);
        System.out.println("序列化 hashCode: "+singleton.hashCode());  
        
        // 反序列化  
        FileInputStream fis = new FileInputStream(file);  
        ObjectInputStream ois = new ObjectInputStream(fis);
        Singleton rSingleton = (Singleton) ois.readObject();
        System.out.println("反序列化 hashCode: "+rSingleton.hashCode());  

        // 資源釋放        
    }  
} 

在反序列化時,ObjectInputStreamreadObject() 的內部代碼執行順序:readObject() --> readObject0() --> readOrdinaryObject() --> invokeReadResolve() --> readResolveMethod.invoke()

invokeReadResolve() 中 使用 readResolveMethod.invoke() 克隆對象。換句話說,invokeReadResolve() 使用反射機制創建新的對象,從而破壞了單例唯一性。

readResolve() 會在 ObjectInputStream 會檢查對象的 class 是否定義了 readResolve()。如果定義了,將由 readResolve() 指定返回的對象。返回對象的類型一定要是兼容的,否則會拋出 ClassCastException

  • 枚舉實現單例模式(線程安全)
public enum EnumSingleton implements SingletonInterface {
	INSTANCE { 
        @Override
        public void doSomething() {
            System.out.println("EnumSingleton singleton");
        }
    };

    public static EnumSingleton getInstance() {
        return EnumSingleton.INSTANCE;
    }
}

public interface SingletonInterface {	
	void doSomething();
}

已知應用

  • java.lang.Runtime
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}

    // ......
}

    @Test
    public void testRuntime() {
        Runtime r1 = Runtime.getRuntime();  
        Runtime r2 = Runtime.getRuntime();  
        //“==”爲地址判斷,爲true表示:r1與r2指向同一對象。  
        System.out.println(r1 == r2);	// 輸出 true
    }

以上爲 java.lang.Runtime 類的部分代碼,是餓漢式單例模式,在該類第一次被 classloader 的時候創建唯一實例。

Runtime 類封裝了 Java 運行時的環境。每一個 Java 程序實際上都是啓動了一個 JVM 進程,每個 Java 程序都有一個 Runtime 類實例,使應用程序能夠與其運行的環境相連接。由於Java 是單進程的,所以,在一個JVM 中,Runtime 的實例應該只有一個。

  • Spring 依賴注入 Bean 實例(默認單例)

相關模式

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章