設計模式之單例模式(Singleton Pattern)

敲程式碼的小小酥發表於2021-03-07

一、定義

    一個類只有一個例項,且該類能自行建立這個例項的一種模式。

二、單例模式舉例

  例如,Windows 中只能開啟一個工作管理員,這樣可以避免因開啟多個工作管理員視窗而造成記憶體資源的浪費,或出現各個視窗顯示內容的不一致等錯誤。

  在計算機系統中,還有 Windows 的回收站、作業系統中的檔案系統、多執行緒中的執行緒池、顯示卡的驅動程式物件、印表機的後臺處理服務、應用程式的日誌物件、資料庫的連線池、網站的計數器、Web 應用的配置物件、應用程式中的對話方塊、系統中的快取等常常被設計成單例。

  J2EE 標準中的ServletContext 和 ServletContextConfig、Spring框架應用中的 ApplicationContext、資料庫中的連線池等也都是單例模式。

三、特點及優缺點

特點:

  1. 單例類只有一個例項物件;

  2. 該單例物件必須由單例類自行建立;

  3. 單例類對外提供一個訪問該單例的全域性訪問點。

優點:

  • 單例模式可以保證記憶體裡只有一個例項,減少了記憶體的開銷。

  • 可以避免對資源的多重佔用。

  • 單例模式設定全域性訪問點,可以優化和共享資源的訪問。

缺點:

  • 單例模式一般沒有介面,擴充套件困難。如果要擴充套件,則除了修改原來的程式碼,沒有第二種途徑,違背開閉原則。

  • 在併發測試中,單例模式不利於程式碼除錯。在除錯過程中,如果單例中的程式碼沒有執行完,也不能模擬生成一個新的物件。

  • 單例模式的功能程式碼通常寫在一個類中,如果功能設計不合理,則很容易違背單一職責原則。

四、單例模式的幾種實現方式

  單例實現把握住一個原則即可:類的建構函式設為私有的,外部類就無法呼叫該建構函式,也就無法生成多個例項。這時該類自身必須定義一個靜態私有例項,並向外提供一個靜態的公有函式用於建立或獲取該靜態私有例項。

要點: 

  1. 構造方法私有化;

  2. 例項化的變數引用私有化;

  3. 獲取例項的方法共有

第1種:餓漢模式

餓漢模式就是在類載入時,就把單例物件載入出來,實現如下:

/**
 * 要點:1.類載入時就建立物件
 *      2.構造方法私有化
 *      3.提供私有成員變數
 *      4.提供對外獲取方法
 */
public class HungrySingleton {
    //類載入時就建立物件
    private static HungrySingleton singleton=new HungrySingleton();
    
    //提供私有構造器
    private HungrySingleton(){
        
    }
    //提供對外獲取方法,一般為靜態
    public static HungrySingleton getInstance(){
        return  singleton;
    }
}

 

第2種:懶漢模式

懶漢模式就是懶載入機制,當有地方用單例物件時,再建立物件,如果一直沒有用,則不建立單例物件。程式碼如下:

/**
 * 要點:1.使用時建立物件
 *      2.構造方法私有化
 *      3.提供私有成員變數
 *      4.提供對外獲取方法,注意執行緒安全問題
 */
public class LazySingletom {
    //建立私有變數,但是不new物件
    private static LazySingletom singletom=null;
    
    //私有構造器
    private LazySingletom(){
        
    }
    //提供對外獲取方法,考慮到執行緒安全,用鎖
    public static synchronized LazySingletom getInstance(){
        if(singletom==null){
            singletom=new LazySingletom();
        }
        return singletom;
    }
}

 

第3種:雙重檢查鎖模式

在懶漢式方式中,synchronized鎖住了整個方法,這影響了效率,針對此問題,設計出了雙重檢查鎖機制

/**
 * 雙重檢查鎖機制:1.使用時建立物件
 *  *      2.構造方法私有化
 *  *      3.提供私有成員變數
 *  *      4.提供對外獲取方法,執行緒安全放在方法內判斷
 */
public class Singleton {
    private static Singleton singleton;

    private Singleton(){

    }

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

 

第4種:列舉實現

利用列舉實現單例,簡單又簡便,程式碼如下:

/**
 * 列舉實現單例模式
 */
public enum EnumSingleton {
    //定義列舉例項,這就是一個單例物件
    INSTANCE;

    /**
     * 列舉是一種特殊的類,可以定義類裡的成員方法,屬性等特徵,可以任意定義東西
     */
    public void getDes(){
        System.out.println("列舉單例模式");
    }

}

 

對於列舉不瞭解的同學,可以閱讀這篇文章熟悉列舉:《JAVA中列舉Enum詳解 》

五、序列化和反射,對單例造成的影響

  上述講解了單例模式的幾種實現方式,但是有些實現方式存在著漏洞,反射和序列化操作,會破壞單例,生成多個物件,下面我們來進行說明和講解。

首先,我們看反射,對上面幾種方式造成的影響。

 我們知道,通過反射,可以獲得類裡的私有屬性,包括私有構造器。所以,無論是惡漢式也好,懶漢式也好,還是雙重檢查鎖模式也好,我們都可以用反射,來獲得其私有構造器,然後進行物件的建立。這樣,我們就可以建立出多個物件了。所以,反射,對這三種模式會造成危害。程式碼如下:

import java.lang.reflect.Constructor;

/**
 * 我們拿餓漢模式來演示反射對單例的破壞
 */
public class ReflectSingleton {
    public static void main(String[] args) throws Exception{
        //通過單例本身拿到單例物件
        HungrySingleton singleton=HungrySingleton.getInstance();
        System.out.println(singleton);
        //通過反射拿到單例物件
       Class clzz= HungrySingleton.class;
        Constructor<HungrySingleton> declaredConstructor = clzz.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        HungrySingleton singletonReflect = declaredConstructor.newInstance();
        System.out.println(singletonReflect);

    }
}

 

執行main方法,檢視執行結果:

 

 可以看到兩個物件的地址值不一致,說明是兩個物件。破壞了單例模式。

那麼我們如何改造呢?就餓漢模式而言,我們在私有構造器裡做判斷,如果私有成員變數不是null,則丟擲異常,阻止通過反射建立新物件,改造後的程式碼如下:

/**
 * 要點:1.類載入時就建立物件
 *      2.構造方法私有化
 *      3.提供私有成員變數
 *      4.提供對外獲取方法
 */
public class HungrySingleton {
    //類載入時就建立物件
    private static HungrySingleton singleton=new HungrySingleton();

    //提供私有構造器
    private HungrySingleton(){
        if(singleton!=null){
            throw new RuntimeException("禁止通過反射建立單例物件");
        }

    }
    //提供對外獲取方法,一般為靜態
    public static HungrySingleton getInstance(){
        return  singleton;
    }
}

 

這樣,我們就可以防止反射破壞餓漢式單例了。但是對於懶漢式和雙重檢查鎖模式,不能這麼改造,來阻止反射破壞單例。因為單例物件不是第一時間建立的,如果第一時間通過反射獲取私有構造,這時私有成員變數是null,那麼,就能通過反射,建立出來物件了。當有程式呼叫單例的getInstance()方法時,又會建立出一個物件,就破壞了單例。所以,對於懶漢式和雙重檢查鎖模式,無法避免反射的危害。

對於列舉模式而言,我們無法通過反射獲取列舉的構造器,因為列舉的構造器,只能通過jvm呼叫。所以,列舉模式無需改造,可以防止單例的破壞。

下面,我們講序列化,對單例造成的影響。如果我們的單例,不需要例項化,則不用考慮該問題,但是如果單例類實現了Serializable介面,則單例模式會有問題。我們來補充一下序列化的知識:

1.每個類可以實現readObjectwriteObject方法實現自己的序列化策略。

2.任何一個readObject方法,不管是顯式的還是預設的,它都會返回一個新建的例項,這個新建的例項不同於該類初始化時建立的例項

3.每個類可以實現private Object readResolve()方法,在呼叫readObject方法之後,如果存在readResolve方法則自動呼叫該方法,readResolve將對readObject的結果進行處理,而最終readResolve的處理結果將作為readObject的結果返回。readResolve的目的是保護性恢復物件,其最重要的應用就是保護性恢復單例、列舉型別的物件。

由上面的,我們可以在單例類裡自定義readResolve方法,返回我們自己定義的單例,來保證序列化對單例沒有影響。

需要注意的是,jdk對列舉型別的序列化,已經做了單例的機制,所以,在列舉模式中,自動規避了序列化造成的問題。

經驗之談:幾種模式中,雖然列舉模式是效果最好,沒有缺陷的一種方式,但是我們沒有必要所有的單例模式都用列舉。如果對效能沒有很高要求,餓漢式是一個不錯的選擇。如果對效能有要求,雙重檢查鎖機制是個不錯的選擇。

 

標題釋出狀態評論數閱讀數操作操作
JAVA中列舉Enum詳解 

相關文章