Java中單例模式的五種實現(xiàn)方式

1,基礎(chǔ)概念

Java中單例模式是一種比較常見的設(shè)計模式,單例模式的種類有:餓漢式單例、懶漢式單例、登記式單例三種。

單例模式的特點:

1,單例類中只能有一個實例

2,單例類必須自己創(chuàng)建自己的唯一實例

3,單例類必須給所有其他對象提供這一實例。

單例模式確保某個類中只有一個實例,而且自行實例化并向整個系統(tǒng)提供這個實例。在計算機系統(tǒng)中,線程池、緩存、日志對象、對話框、打印機、顯卡的驅(qū)動程序?qū)ο蟪1辉O(shè)計成單例。這些應(yīng)用都或多或少具有資源管理器的功能。每臺計算機可以有若干通信端口,系統(tǒng)應(yīng)當(dāng)集中管理這些通信端口,以避免一個通信端口同時被兩個請求同時調(diào)用??傊x擇單例模式就是為了避免不一致狀態(tài),避免政出多頭。

單例模式的好處:

1,它能夠避免對象的重復(fù)創(chuàng)建,不僅可以減少每次創(chuàng)建對象的時間開銷,還可以節(jié)約內(nèi)存空間。

2,能夠避免由于操作多個實例導(dǎo)致的邏輯錯誤。如果一個對象有可能貫穿整個應(yīng)用程序,而且起到了全局統(tǒng)一管理控制的作用,那么單例模式也許是一個值得考慮的選擇。

單例模式有很多種實現(xiàn)方式,下面會對這幾種實現(xiàn)方式逐一介紹。

2,餓漢模式

public class Singleton{
    private static final Singleton singleton = new Singleton();
    private Singleton(){}
    public static Singleton newInstance(){
        return singleton;
    }
}

從代碼種可以看到,這個類的構(gòu)造函數(shù)是私有的,所以保證其他類不能實例化這個類,然后提供了一個靜態(tài)實例并返回給調(diào)用者。餓漢模式是最簡單的一種設(shè)計單例模式,在類加載的時候就創(chuàng)建實例,實例在整個程序周期都會存在。

餓漢模式的優(yōu)缺點:

優(yōu)點:在類加載的時候創(chuàng)建一次實例,不會存在多個線程創(chuàng)建多個實例的情況,避免了多線程同步的問題。

缺點:它的缺點也很明顯,即使這個單例沒有用到也會被創(chuàng)建,而且在類加載之后就被創(chuàng)建,內(nèi)存就被浪費了。

餓漢模式使用場景:

這種實現(xiàn)方式適合單例占用內(nèi)存比較小,在初始化時就會被用到的情況。但是,如果單例占用的內(nèi)存比較大,或單例只是在某個特定場景下才會用到,使用餓漢模式就不合適了,這時候就需要用到懶漢模式進行延遲加載。

3,懶漢模式

public class Singleton{
    private static final Singleton singleton = null;
    private Singleton(){}
    public static Singleton newInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

懶漢模式就是在需要的時候進行實例化,如果單例已經(jīng)創(chuàng)建,再次調(diào)用獲取接口將不會重新創(chuàng)建新的對象,而是直接返回之前創(chuàng)建的對象。如果某個單例使用的次數(shù)少,并且創(chuàng)建單例消耗的資源較多,那么就需要實現(xiàn)單例的按需創(chuàng)建,這個時候使用懶漢模式就是一個不錯的選擇。

但是這種懶漢模式有一個致命的缺點,那就是安全性沒法保證。在多個線程可能會并發(fā)調(diào)用它的getInstance()方法,導(dǎo)致創(chuàng)建多個實例,因此需要加鎖解決線程同步問題,實現(xiàn)如下。

public class Singleton{
    private static final Singleton singleton = null;
    private Singleton(){}
    public static synchronized Singleton newInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

4,雙重校驗鎖

加鎖的懶漢模式看起來即解決了線程并發(fā)問題,又實現(xiàn)了延遲加載,然而它存在著性能問題,依然不夠完美。synchronized修飾的同步方法比一般方法要慢很多,如果多次調(diào)用getInstance(),累積的性能損耗就比較大了。因此就有了雙重校驗鎖,先看下它的實現(xiàn)代碼。

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

可以看到上面在同步代碼塊外多了一層singleton為空的判斷。由于單例對象只需要創(chuàng)建一次,如果后面再次調(diào)用getInstance()只需要直接返回單例對象。因此,大部分情況下,調(diào)用getInstance()都不會執(zhí)行到同步代碼塊,從而提高了程序性能。不過還需要考慮一種情況,假如兩個線程A、B,A執(zhí)行了if (singleton== null)語句,它會認(rèn)為單例對象沒有創(chuàng)建,此時線程切到B也執(zhí)行了同樣的語句,B也認(rèn)為單例對象沒有創(chuàng)建,然后兩個線程依次執(zhí)行同步代碼塊,并分別創(chuàng)建了一個單例對象。為了解決這個問題,還需要在同步代碼塊中增加if (singleton== null)語句,也就是上面看到的代碼2。

我們看到雙重校驗鎖即實現(xiàn)了延遲加載,又解決了線程并發(fā)問題,同時還解決了執(zhí)行效率問題,是否真的就萬無一失了呢?

這里要提到Java中的指令重排優(yōu)化。所謂指令重排優(yōu)化是指在不改變原語義的情況下,通過調(diào)整指令的執(zhí)行順序讓程序運行的更快。JVM中并沒有規(guī)定編譯器優(yōu)化相關(guān)的內(nèi)容,也就是說JVM可以自由的進行指令重排序的優(yōu)化。

這個問題的關(guān)鍵就在于由于指令重排優(yōu)化的存在,導(dǎo)致初始化Singleton和將對象地址賦給singleton字段的順序是不確定的。在某個線程創(chuàng)建單例對象時,在構(gòu)造方法被調(diào)用之前,就為該對象分配了內(nèi)存空間并將對象的字段設(shè)置為默認(rèn)值。此時就可以將分配的內(nèi)存地址賦值給singleton字段了,然而該對象可能還沒有初始化。若緊接著另外一個線程來調(diào)用getInstance(),取到的就是狀態(tài)不正確的對象,程序就會出錯。

以上就是雙重校驗鎖會失效的原因,不過還好在JDK1.5及之后版本增加了volatile關(guān)鍵字。volatile的一個語義是禁止指令重排序優(yōu)化,也就保證了singleton變量被賦值的時候?qū)ο笠呀?jīng)是初始化過的,從而避免了上面說到的問題。代碼如下:

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

5,靜態(tài)內(nèi)部類

除了上面的三種方式,還有另外一種實現(xiàn)單例的方式,通過靜態(tài)內(nèi)部類來實現(xiàn)。首先看一下它的實現(xiàn)代碼:

public class Singleton{
    private static class SingletonHolder{
        public static Singleton instance = new Singleton();
    }
    private Singleton(){}
    public static Singleton newInstance(){
        return SingletonHolder.instance;
    }
}

這種方式同樣利用了類加載機制來保證只創(chuàng)建一個singleton實例。它與餓漢模式一樣,也是利用了類加載機制,因此不存在多線程并發(fā)的問題。不一樣的是,它是在內(nèi)部類里面去創(chuàng)建對象實例。這樣的話,只要應(yīng)用中不使用內(nèi)部類,JVM就不會去加載這個單例類,也就不會創(chuàng)建單例對象,從而實現(xiàn)懶漢式的延遲加載。也就是說這種方式可以同時保證延遲加載和線程安全。

上面四種實現(xiàn)單例模式的方式都有共同的缺點:

1,需要額外的工作來實現(xiàn)序列化,否則每次反序列化一個序列化的對象時都會創(chuàng)建一個新的實例。

2,可以使用反射強行調(diào)用私有構(gòu)造器(如果要避免這種情況,可以修改構(gòu)造器,讓它在創(chuàng)建第二個實例的時候拋異常)。

而枚舉類很好的解決了這兩個問題,使用枚舉除了線程安全和防止反射調(diào)用構(gòu)造器之外,還提供了自動序列化機制,防止反序列化的時候創(chuàng)建新的對象。

6,枚舉

再來看下最后一種實現(xiàn)方式:枚舉。

public enum Singleton{
    instance;
    public void whateverMethod(){}    
}

7,小結(jié)

本篇文章介紹了Java中單例模式的五種實現(xiàn)方式,在沒有特殊需求的情況下,個人建議使用雙重校驗鎖靜態(tài)內(nèi)部類實現(xiàn)單例模式。由于純手打,難免會有紕漏,如果發(fā)現(xiàn)錯誤的地方,請第一時間告訴我,這將是我進步的一個很重要的環(huán)節(jié)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容