天天看點

設計模式:單例模式節省資源,提高系統性能

一、問題場景

針對唯一遞增ID号碼生成器,如果需要時就直接建立一個ID對象,當程式中有兩個對象,那就會存在生成重複ID的情況,此外頻繁建立ID對象,也會消耗記憶體空間。

二、問題分析

ID生成器類是全局使用的類,會被頻繁建立和銷毀,為了節省記憶體空間,就要控制建立對象的數量,保證一個類隻有一個對象,并提供一個全局的通路點。這就是建立型設計模式中的單例模式。

三、單例定義

單例設計模式指一個類隻允許建立一個對象,并提供一個全局通路該對象的靜态方法。

四、單例實作方式

為了保證這個對象隻有一個,不能被外部程式建立,這個對象的構造函數必須是私有的,隻能目前類内部建立。

1、餓漢式

public class SingleTon {

    /**
     * 将構造器私有化,防止直接new
     */
    private SingleTon(){

    }


    /**
     * 方式一:靜态常量方式
     * SingleTon對象執行個體在定義靜态常量時建立
     * 在類裝載時就完成執行個體化
     */
    private static SingleTon instance = new SingleTon();

//    /**
//     * 方式二: 靜态代碼塊方式
//     * SingleTon對象執行個體的建立放在靜态代碼塊中
//     * 當類裝載的時候,就執行靜态代碼塊中的代碼,初始化SingleTon對象
//     */
//    private static SingleTon instance;
//    static {
//        instance = new SingleTon();
//    }

    /**
     * 提供一個public的靜态方法,傳回instance
     * @return
     */
    public static SingleTon getInstance(){
        return instance;
    }

}           

在類加載時建立并初始化好對象。這種建立是線程安全的,不過不支援延遲加載。

結論:不推薦實際開發中使用

2、懶漢式

public class SingleTonLazyLoading {

    /**
     * 将構造器私有化,防止直接new
     */
    private SingleTonLazyLoading(){

    }

    private static SingleTon instance;


    /**
     * 調用擷取單例對象方法時,再建立對象,實作延遲加載
     * 加鎖和判空是為了線程安全,防止二次建立對象
     * @return
     */
    public static Synchronized SingleTonLazyLoading getInstance(){
        if(instance == null){
            instance = new SingleTon();
        }
        return instance;
    }

}           

為了支援懶加載,在擷取單例對象方法時再建立對象;

為了支援線程安全,隻建立一個對象,在擷取單例對象方法上加鎖synchronized,同時方法裡判斷對象是否存在,存在則直接傳回。

懶漢式支援懶加載,但是每次調用該方法,需要頻繁加鎖,釋放鎖,有并發度低的問題。

結論:不推薦實際開發中使用

3、雙重檢測

public class SingleTonDoubleCheck {

    /**
     * 将構造器私有化,防止直接new
     */
    private SingleTonDoubleCheck(){

    }

    private static SingleTon instance;


    public static  SingleTonDoubleCheck getInstance(){
        if(instance == null){
            synchronized (SingleTonDoubleCheck.class){
                if(instance == null){
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }

}           

懶漢式并發度低,是因為頻繁加鎖和釋放鎖,可以将方法鎖替換成同步代碼塊鎖來縮小加鎖的範圍,進而降低加鎖的頻率。

第一次檢測,是為了判斷對象是否存在,存在則直接傳回對象。

第二次檢測,是為了防止二次建立對象。如果兩個線程都通過了第一次檢測,其中一個線程先擷取鎖,建立好對象之後釋放鎖。然後,第二個線程擷取到鎖,如果不加第二次檢測,就會二次建立對象。

這種實作方式解決了并發度低的問題,既支援延遲加載,也支援高并發。

結論:推薦實際開發中使用

4、靜态内部類

public class SingleTonStaticInnerClass {

    /**
     * 将構造器私有化,防止直接new
     */
    private SingleTonStaticInnerClass(){

    }

    private static class SingleTonInstance {
        private static final SingleTonStaticInnerClass instance = new SingleTonStaticInnerClass();
    }

    public static SingleTonStaticInnerClass getInstance(){
        return SingleTonInstance.instance;
    }

}           

餓漢式保證了線程安全,但是不支援延遲加載。

要想實作延遲加載,還可以利用靜态内部類的加載特點:在外部類加載的時候,靜态内部類

并不會被加載,隻有在程式中調用靜态内部類的時候才加載。

此外,類裝載的機制又保證初始化執行個體時隻有一個線程

靜态内部類實作方式,既避免了線程不安全,又實作了延遲加載,效率高

結論:推薦實際開發中使用

5、枚舉

public class Singleton {
    private Singleton(){
    }
    public static enum SingletonEnum {
        SINGLETON;
        private Singleton instance = null;
        private SingletonEnum(){
            instance = new Singleton();
        }
        public Singleton getInstance(){
            return instance;
        }
    }
}           

因為Java虛拟機會保證枚舉類型不能被反射并且構造函數隻被執行一次。

這種借助JDK1.5中添加的枚舉來實作的單例模式,不僅能避免多線程同步問題,而且還能防

止反序列化重新建立新的對象。

結論:推薦實際開發中使用

五、單例的應用

在JDK中,java.lang.Runtime就是經典的單例模式(餓漢式)

/**
 * Every Java application has a single instance of class
 * <code>Runtime</code> that allows the application to interface with
 * the environment in which the application is running. The current
 * runtime can be obtained from the <code>getRuntime</code> method.
 * <p>
 * An application cannot create its own instance of this class.
 *
 * @author  unascribed
 * @see     java.lang.Runtime#getRuntime()
 * @since   JDK1.0
 */
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

    //....
    public void addShutdownHook(Thread hook) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("shutdownHooks"));
        }
        ApplicationShutdownHooks.add(hook);
    }
    //...
}           

六、單例的使用場景

1、需要頻繁的進行建立和銷毀的對象、建立對象時耗時過多或耗費資源過多(重量級對象),但又經常用到的對象、工具類對象、頻繁通路資料庫或檔案的對象(比如資料源、session工廠等

2、表示全局唯一的類。從業務概念上,如果有些資料在系統中隻應儲存一份,那就比較适合設計為單例類。

比如,配置資訊類。在系統中,我們隻有一個配置檔案,當配置檔案被加載到記憶體之後,以對象的形式存在,也理所應當隻有一份。

七、問題實作

單例模式已經講解清楚,那開篇提到的唯一遞增ID對象的單例模式怎麼設計實作呢?你自己可以先試試實作一下,下期文章揭曉答案。

繼續閱讀