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


一、定義

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

二、單例模式舉例

  例如,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詳解 


免責聲明!

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



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