程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> [設計模式]單例模式

[設計模式]單例模式

編輯:JAVA綜合教程

[設計模式]單例模式


1.單例模式介紹

單利模式是應用最廣的模式之一,也可能是很多初級工程師唯一會使用的設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只需要擁有一個全局對象,這樣有利於我們協調系統整體的行為。如在一個應用中,應該只有一個ImageLoader實例,這個ImageLoader中又含有線程池、緩存系統、網絡請求等,很消耗資源,因此,沒有理由讓他構造多個實例。

這種不能自由構造對象的情況,就是單利模式的使用場景

2.單例模式的定義

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

3.單例模式的使用場景

確保某個類有且只有一個對象的場景,避免產生多個對象消耗過多的資源,或者某種類型的對象只應該有且只有一個。

例如,創建一個對象需要消耗的資源過多,如果要訪問IO和數據庫等資源,這時就要考慮使用單例模式了

4.單例模式UML類圖

這裡寫圖片描述

角色介紹:
(1)Client——-高層客戶端;
(2)Singleton——單例類

實現單例模式有如下幾個關鍵點:
(1)構造函數不對外開放,一般為Private;
(2)通過一個靜態方法或者枚舉返回單例類對象
(3)確保單例類的對象有且只有一個,尤其是在多線程環境下;
(4)確保單例類對象在反序列化時不會重新構建對象。

通過將單例類的構造函數私有化,使得客戶端代碼不能通過new的形式手動構造單例類的對象。單例類會暴露一個共有靜態方法,客戶端需要調用這個靜態方法獲取到單例類的唯一對象,在或許這個單例對象的過程中需要確保線程安全,及在多線程環境下構造單例類的對象也是有且只有一個,這也是單例模式實現中比較困難的地方。

5.單例模式的簡單示例

public class Staff{
    public void work(){
        //干活
    }
}

public class VP extends Staff{
    @Overrride
    public void work{
        //管下面的經理
    }
}

public static class CEO extends Staff{
    private static final CEO mCeo = new CEO;
    //構造函數
    private CEO(){
    }
    //公用的靜態函數,對外暴露獲取單例對象的接口
    public static CEO getCEO(){
        return mCeo;
    }
    @Override
    public void work(){
        //管理VP
    }
}

public class Company{
    private List allStaffs = new ArrayList();
    public void addStaff(Staff per){
        allStaffs.add(per);
    }
    public void showAllStaffs(){
        for(Staff per : allStaffs){
            System.out.println("Obj"+per.toString());
        }
    }
}

public class Test{
    public static void main(String[] args){
        Company cp = new Company();

        Staff ceo1 = CEO.getCEO();
        Staff ceo2 = CEO.getCEO();

        cp.addStaff(ceo1);
        cp.addStaff(ceo2);

        Staff vp1 = new VP();
        Staff vp2 = new VP();

        Staff staff1 = new Staff();
        Staff staff2 = new Staff();
        Staff staff3 = new Staff();

        cp.addStaff(vp1);
        cp.addStaff(vp2);
        cp.addStaff(staff1);
        cp.addStaff(staff2);
        cp.addStaff(staff3);

        cp.showAllStaffs();



    }
}

輸出結果可以看到CEO類不能通過new的形式構造對象,只能通過CEO.getCEO()函數來獲取,而這個CEO對象是靜態對象,並且在生命的時候就已經初始化,這就保證了CEO對象的唯一性。從輸出結果中發現,CEO兩次輸出的CEO對象都是一樣的,而VP,Staff等類型的都是不同的,這個實現的核心在於將CEO累的構造方法私有化,使得外部程序不能通過構造函數來構造CEO對象,而CEO類通過一個靜態方法返回一個靜態對象。

6.單例模式的其他實現方式

6.1 懶漢模式

懶漢模式是聲明一個靜態對象,並且在用戶第一次調用getInstance時進行初始化,而上述的餓漢模式(CEO類)是在聲明靜態對象時已經初始化。

懶漢單例模式實現如下:

public class Singleton{
    private static Singleton instance;
    private Singleton(){}

    public static synchronized Sigleton getInstance(){

        if(instance == null){
            instance = new Singleton();
        }

        return instance;
    }
}

getInstance()加了一個synchronized關鍵字,也就是說getInstance()是一個同步方法,這就是上面所說的在多線程情況下保證單例對象唯一性的手段。

這時就有問題了,即使instance已經被初始化(第一次調用時就會被初始化instance),每次調用getInstance都會進行同步,這樣會消耗不必要的資源,這也就是懶漢單例模式的問題。

最後總結一下,懶漢單例模式的優點是單裡只有在使用時才會被實例化,在一定程度上節約資源。
缺點是第一次加載時需要及時進行實例化,反應稍慢,最大的問題是每次調用getInstance都進行同步,造成不必要的同步開銷。

這種模式一般不是很建議使用。

6.2 Double CheckLock(DCL)實現單例

DCL方式實現單例模式的優點是既能夠在需要時才初始化單例模式,又能保證線程安全,且單例對象初始化後調用getInstance不進行同步鎖。

代碼如下所示:

public class Singleton{
    private static Singleton sInstance = null;
    private Singleton(){
    }
    public void doSometing(){
        System.out.println("do sth.");
    }
    public static Singleton getInstance(){
        if(mInstahce == null){
            synchronized(Singleton.class){
                if(mInstance == null){
                    sInstance = new Singleton();
                }
            }
        }
        return sInstance;
    }
}

本程序的亮點自然都在getInstance方法上,可以看到getInstance方法對instance進行了兩次判斷:第一層判斷主要是為了避免不必要的同步,第二層判斷則是為了在null的情況下創建實例,這是什麼意思呢?

首先我們要知道sInstance = new Singleton()這個語句,它大致做了三件事情:
(1)給Singleton的實例分配內存;
(2)調用Singleton()的構造函數,初始化成員字段;
(3)將sInstance對象指向分配的內存空間(此時sInstance就不是null了)。

在JDK1.5之前的JMM長Cache、寄存器到主內存回寫順序的規定,上面的第二和第三順序是無法保證的,也就是說,執行順序可能是1-2-3也可能是1-3-2。如果是後者,並且在3執行完畢,2未執行之前,被切換到線程B上,這時候sInstance因為已經在線程A內執行過了第三點,sInstance已經是非空了,所以,線程B直接取走sInstance,再使用時就會出錯,這就是DCL失效的問題,而且這種難以跟蹤。

JDK1.5之後SUN公司調整了JMM,具體化了volatile關鍵字,因此,只需要將sInstance的定義改成private volatile static Singleton sInstance = null就可以保證sInstance對象每次都是從主內存中讀取,就可以使用DCL的寫法來完成單例模式,當然使用volatile或多或少也會影響到性能。

DCL的有點:資源利用率高,第一次執行getInstance是時單例對象才會被實例化,效率高。
缺點:第一次加載時反應有點慢,也由於java的內存模型的原因偶爾會失敗,在高並發環境下也有一定的缺陷,雖然發生的概率很小。DCL模式是使用最多的單例實現方式,它能夠在需要時才實例化單例對象,並且能夠在絕大多數場景下保證單例對象的唯一性。除非代碼在並發場景比較復雜。

6.3 靜態內部類單例模式

DCL雖然在一定程度上解決了資源消耗、多余的同步、線程安全等問題但是,他還是在某下情況下出現時效的問題,這個問題被稱為雙重檢查鎖定失效,在《java並發編程實踐》這一書中有討論。
建議使用如下代碼進行替換

public class Singleton{
    private Singleton(){}
    public static Singleton getInstance(){
        return SingletonHolder.sInstance;
    }
    //靜態內部類
    private static class SingletonHolder{
        private static final Singleton sInstance = new Singleton();
    }
}

當第一次加載Singleton類時並不會初始化sInstance,只有在第一次調用Singleton的getInstance方法時才會導致sInstance被初始化。因此,第一次調用getInstance方法會導致虛擬機加載SingletonHolder類,這種方式不僅能夠保證線程安全,也能夠保證單例對象的唯一性,同事也延遲了單例的實例化,所以這是推薦使用單例模式的實現方式。

6.4 枚舉單例

枚舉就是最簡單的單例實現方法

public enum SingleEnum{
    INSTANCE;
    public void doSomething{
        System.out.println("do sth.");
    }
}

寫法簡單就是枚舉最大的優點,枚舉在Java中與普通的類是一樣的,不僅能夠有字段,還能夠有自己的方法。更重要的是默認枚舉實例的創建是線程安全的,並且在任何情況下它都是一個單例。

6.5使用容器實現單例模式

public class SingletonManager{
    private static Map objMap = new HashMap();

    private Singleton(){}
    public static void registerService(String key,Object instance){
        if (!objMap.containKey(key)) {
            objMap.put(key,instance);

        }
    }
    public static ObjectgetService(String key){
        return objMap.get(key);
    }
}

這種形式可以將多種單例類型注入到一個統一的管理類中,在使用時根據key獲取對應類型的對象。這種方式使得我們可以管理多種類型的單例,並且在使用時可以通過統一的接口進行獲取操作,降低了用戶的使用成本也對用戶隱藏了具體實現,降低了耦合度。

總而言之,不管以那種形式實現單例模式,他的核心原理都是將構造函數私有化,並且通過靜態方法獲取一個唯一的實例,在這個獲取的過程中必須保證線程安全、防止反序列化導致重新生成實例對象等問題。

   

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved