設計模式—單例模式的六種寫法


一、定義

  確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例

二、UML結構圖

三、場景

  • 需要頻繁的實例化和銷毀的對象;
  • 有狀態的工具類對象
  • 頻繁訪問數據庫或文件對象;
  • 確保某個類只有一個對象的場景,比如一個對象需要消耗的資源過多,訪問io、數據庫,需要提供全局配置的場景 

四、幾種單例模式

1、餓漢式

   聲明靜態時已經初始化,在獲取對象之前就初始化

  優點:獲取對象的速度快,線程安全(因為虛擬機保證只會裝載一次,在裝載類的時候是不會發生並發的)

  缺點:耗內存(若類中有靜態方法,在調用靜態方法的時候類就會被加載,類加載的時候就完成了單例的初始化,拖慢速度)

/**
 * 單例模式:餓漢式
 * 在類加載的時候就已經完成了初始化,所以類加載較慢,但獲取對象的速度快
 * @author Administrator
 *
 */
public class EagerSingleton {

    //靜態私有成員,已初始化
    private static EagerSingleton instance = new EagerSingleton();
    
    
    //私有構造函數
    private EagerSingleton() {
        
    }
    
        
    //靜態,不用同步(類加載時已初始化,不會有多線程的問題)
    public static EagerSingleton getInstance() {
        return instance;
    }
    
}

2、懶漢式

  synchronized同步鎖: 多線程下保證單例對象唯一性

  優點:單例只有在使用時才被實例化,一定程度上節約了資源

  缺點:加入synchronized關鍵字,造成不必要的同步開銷。不建議使用。

/**
 * 單例模式:懶漢式(線程安全的懶漢式)
 * 比較懶,在類加載時,不創建實例,因此類加載速度快,但運行時獲取對象的速度慢
 * @author Administrator
 *
 */
public class LazySingleton {

    //靜態私有成員,沒有初始化
    private static LazySingleton instance = null;
    
    
    //私有構造函數
    private LazySingleton() {
        
    }
    
    
    //靜態,同步,公開訪問點
    public static synchronized LazySingleton getInstace() {
        if(instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

3、Double Check Lock(DCL)實現單例(使用最多的單例實現之一)

  雙重鎖定體現在兩次判空

  優點:既能保證線程安全,且單例對象初始化后調用getInstance不進行同步鎖,資源利用率高

  缺點:第一次加載稍慢,由於Java內存模型一些原因偶爾會失敗,在高並發環境下也有一定的缺陷,但概率很小。

/**
 * 單例模式:雙重鎖定式
 * @author Administrator
 *
 */
public class SingletonKerriganD {

    //這里加volatitle是為了避免DCL失效
    private volatile static SingletonKerriganD instance = null;
    
    
    //私有構造函數
    private SingletonKerriganD() {
        
    }
    
    
    /**
     * DCL對instance進行了兩次null判斷
     * 第一層判斷主要是為了避免不必要的同步
     * 第二層判斷則是為了在null的情況下創建實例
     * @return
     */
    public static SingletonKerriganD getInstance() {
        if(instance == null) {
            synchronized (SingletonKerriganD.class) {
                if(instance == null) {
                    instance = new SingletonKerriganD();
                }
            }
        }
        return instance;
    }
}

  什么是DCL失效問題?

  假如線程A執行到instance = new SingletonKerriganD(),大致做了如下三件事:

  1. 給實例分配內存
  2. 調用構造函數,初始化成員字段
  3. 將instance 對象指向分配的內存空間(此時sInstance不是null)

  如果執行順序是1-3-2,那多線程下,A線程先執行3,2還沒執行的時候,此時instance!=null,這時候,B線程直接取走instance ,使用會出錯,難以追蹤。JDK1.5及之后的volatile 解決了DCL失效問題(雙重鎖定失效)

4、靜態內部類單例模式
   在調用 SingletonHolder.instance 的時候,才會對單例進行初始化

  優點:線程安全、保證單例對象唯一性,同時也延遲了單例的實例化

  缺點:需要兩個類去做到這一點,雖然不會創建靜態內部類的對象,但是其 Class 對象還是會被創建,而且是屬於永久代的對象。

/**
 * 單例模式:靜態內部類式
 * @author Administrator
 *
 */
public class SingletonInner {

    //私有構造函數
    private SingletonInner() {
        
    }
    
    
    //在調用SingletonHolder.instance的時候,才會對單例進行初始化
    public static class SingletonHolder{
        private final static SingletonInner instance = new SingletonInner();
    }
    
    
    public static SingletonInner getInstance() {
        return SingletonHolder.instance;
    }
}

  這種方式如何保證單例且線程安全?

  當getInstance方法第一次被調用的時候,它第一次讀取SingletonHolder.instance,內部類SingletonHolder類得到初始化;而這個類在裝載並被初始化的時候,會初始化它的靜態域,從而創建Singleton的實例,由於是靜態的域,因此只會在虛擬機裝載類的時候初始化一次,並由虛擬機來保證它的線程安全性。 這個模式的優勢在於,getInstance方法並沒有被同步,並且只是執行一個域的訪問,因此延遲初始化並沒有增加任何訪問成本。

  這種方式能否避免反射入侵?

  答案是:不能。網上很多介紹到靜態內部類的單例模式的優點會提到“通過反射,是不能從外部類獲取內部類的屬性的。 所以這種形式,很好的避免了反射入侵”,這是錯誤的,反射是可以獲取內部類的屬性(想了解更多反射的知識請看 java反射全解),入侵單例模式根本不在話下

【注意】:上述四種方法要杜絕在被反序列化時重新聲明對象,需要加入如下方法:
private Object readResolve() throws ObjectStreamException{
    return sInstance;
}

  為什么呢?因為當JVM從內存中反序列化地"組裝"一個新對象時,自動調用 readResolve方法來返回我們指定好的對象

5、枚舉單例

  優點:線程安全,防止被反序列化

  缺點:枚舉相對耗內存

public enum  SingletonEnum {
    instance;
    public void doThing(){
        
    }

}

  只要 SingletonEnum.INSTANCE 即可獲得所要實例。

  這種方式如何保證單例?

  首先,在枚舉中我們明確了構造方法限制為私有,在我們訪問枚舉實例時會執行構造方法,同時每個枚舉實例都是static final類型的,也就表明只能被實例化一次。在調用構造方法時,我們的單例被實例化。 也就是說,因為enum中的實例被保證只會被實例化一次,所以我們的INSTANCE也被保證實例化一次

  上面示例中生成的字節碼文件對instance的描述如下:
...
public static final eft.reflex.SingletonEnum instance;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM


...

  可以看出,會自動生成 ACC_STATIC, ACC_FINAL這兩個修飾符

  枚舉類型為什么是線程安全的?

  我們定義的一個枚舉,在第一次被真正用到的時候,會被虛擬機加載並初始化,而這個初始化過程是線程安全的。而我們知道,解決單例的並發問題,主要解決的就是初始化過程中的線程安全問題。所以,由於枚舉的以上特性,枚舉實現的單例是天生線程安全的。

6、使用容器實現單例模式
  在程序的初始化,將多個單例類型注入到一個統一管理的類中,使用時通過key來獲取對應類型的對象,這種方式使得我們可以管理多種類型的單例,並且在使用時可以通過統一的接口進行操作。這種方式是利用了Map的key唯一性來保證單例。
import java.util.HashMap;
import java.util.Map;

/**
 * 單例模式:容器模式
 * @author Administrator
 *
 */
public class SingletonManager {

    private static Map<String, Object> map = new HashMap<String, Object>();
    
    private SingletonManager() {
        
    }
    
    public static void registerService(String key, Object instance) {
        if(!map.containsKey(key)) {
            map.put(key, instance);
        }
    }
    
    public static Object getService(String key) {
        return map.get(key);
    }
}

五、總結

所有單例模式需要處理得問題都是:

  1. 將構造函數私有化
  2. 通過靜態方法獲取一個唯一實例
  3. 保證線程安全
  4. 防止反序列化造成的新實例等。

推薦使用:DCL、靜態內部類、枚舉

單例模式優點

  1. 只有一個對象,內存開支少、性能好(當一個對象的產生需要比較多的資源,如讀取配置、產生其他依賴對象時,可以通過應用啟動時直接產生一個單例對象,讓其永駐內存的方式解決)
  2. 避免對資源的多重占用(一個寫文件操作,只有一個實例存在內存中,避免對同一個資源文件同時寫操作)
  3. 在系統設置全局訪問點,優化和共享資源訪問(如:設計一個單例類,負責所有數據表的映射處理)

單例模式缺點

  1. 一般沒有接口,擴展難
  2. android中,單例對象持有Context容易內存泄露,此時需要注意傳給單例對象的Context最好是Application Context

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM