單例模式

概述

單例模式是比較簡(jiǎn)單的設(shè)計(jì)模式,使用也是非常的廣泛??匆幌玛P(guān)于單例模式的定義:

確保一個(gè)類只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。

它的定義包含了三個(gè)要點(diǎn):

  • 一個(gè)類只有一個(gè)實(shí)例:外部程序無法通過new關(guān)鍵字來創(chuàng)建實(shí)例,構(gòu)造方法用private修飾
  • 自行實(shí)例化:由類本身創(chuàng)建實(shí)例對(duì)象,為了外界可以訪問這個(gè)實(shí)例,需要定義成靜態(tài)的私有成員變量。
  • 提供實(shí)例: 因?yàn)闊o法提供new來床架對(duì)象,只能提供一個(gè)公有靜態(tài)方法,返回創(chuàng)建的對(duì)象實(shí)例。

來看一下單例模式的通用代碼:

餓漢式單例

class Singleton {
    private static final Singleton singletonTest = new Singleton();  //創(chuàng)建實(shí)例
    private Singleton() {}   //構(gòu)造方法私有

    public static Singleton getInstance(){
        return singletonTest ;  //返回實(shí)例
    }
}

public class SingletonTest{
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance() ;
        Singleton s2 = Singleton.getInstance();

        System.out.println(s1 == s2);  //true
    }
}

上面這種寫法是最簡(jiǎn)單的寫法,但是有一個(gè)問題,就是在類加載的時(shí)候就創(chuàng)建了實(shí)例,而不管是否需要?jiǎng)?chuàng)建這個(gè)實(shí)例,沒有做到延遲加載 ,增加了負(fù)載。

懶漢式單例

為了解決上面的問題,對(duì)上面的通用代碼進(jìn)行如下改進(jìn),當(dāng)系統(tǒng)需要該實(shí)例的時(shí)候,才產(chǎn)生實(shí)例對(duì)象,并返回,代碼如下:

class LazySingleton{

    private static  LazySingleton  singleton = null ;

    private LazySingleton(){}

    public static LazySingleton getInstance(){
        if(singleton == null ){
            singleton = new LazySingleton() ;
        }
        return  singleton ;
    }
}

通過上面的代碼發(fā)現(xiàn),在類加載的時(shí)候并沒有進(jìn)行類的初始化操作,只有在第一次調(diào)用getInstance()方法時(shí),才進(jìn)行了實(shí)例化操作,實(shí)現(xiàn)了延遲加載的功能。那么問題又來了,啥問題問題,上面的類在單線程環(huán)境是沒有問題的,但是在多線程環(huán)境中是不安全的無法保證實(shí)例的唯一性,那么在改進(jìn)一下,看下面代碼:

class LazySingleton{

    private static  LazySingleton  singleton = null ;

    private LazySingleton(){}

    public static synchronized LazySingleton getInstance(){
        if(singleton == null ){
            singleton = new LazySingleton() ;
        }
        return  singleton ;
    }
}

在getInstance的方法上添加了synchronized關(guān)鍵字,來保證線程之間的同步,確保實(shí)例的唯一性??雌饋硪呀?jīng)解決了上面的問題,但是新的問題其實(shí)又出現(xiàn)了,什么問題呢? 就是每次訪問getInstance()方法時(shí)都進(jìn)進(jìn)行線程鎖定的判斷,在多線程高并發(fā)的環(huán)境下,使得系統(tǒng)的性能大大的降低了,那么有什么辦法來解決這個(gè)問題嗎? 當(dāng)然有,看下面代碼:

class LazySingleton{

    private static  LazySingleton  singleton = null ;

    private LazySingleton(){}

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

        }
        return  singleton ;
    }
}

最初為了保證線程之間的同步問題,給getInstance()的整個(gè)方法加了鎖,其實(shí)要保證同步的罪魁禍?zhǔn)灼鋵?shí)是 singleton=new LazySingleton()這句,只要保證了它的同步,產(chǎn)生的實(shí)例就是唯一的,所以使用synchronized塊,來保證線程之間的同步。 這樣在完成了singleton初始化之后,便不會(huì)在進(jìn)入synchronized中,系統(tǒng)的性能便不會(huì)受影響了。 程序看起來已經(jīng)很完美了,事實(shí)確實(shí)是這樣嗎? 當(dāng)然不是,再次被打臉了,分析一下,假設(shè)現(xiàn)在有兩個(gè)線程A ,B 都執(zhí)行到了 if() 出,線程A先執(zhí)行了,獲得了鎖,此時(shí)完成了singleton的初始化操作,釋放鎖 ,B開始執(zhí)行,因?yàn)锽并不知道此時(shí)singleton已經(jīng)完成了實(shí)例的創(chuàng)建,會(huì)再次創(chuàng)建一個(gè)實(shí)例。 好的,我們?cè)诟倪M(jìn)一下 :

class LazySingleton{

    private volatile static  LazySingleton  singleton = null ;

    private LazySingleton(){}

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

        }
        return  singleton ;
    }
}

在synchronized塊中,又進(jìn)行了一次條件判斷,這種方式稱為雙重檢查鎖定。 另外在變量的聲明中添加了volatile關(guān)鍵字。關(guān)于volatile簡(jiǎn)單說兩句,由于singleton使用了volatile修飾,一旦一個(gè)線程對(duì)改變做了修改,會(huì)馬上由工作內(nèi)存寫回到主內(nèi)存中,被其他線程所讀取,工作內(nèi)存可以理解為被線程獨(dú)享,主內(nèi)存可以理解為線程共享,被volatile修飾的成員變量可以確保多個(gè)線程都能夠正確的處理,但是該代碼只能在jdk1.5及以上的版本中才能正確執(zhí)行?,F(xiàn)在9都快要發(fā)布了, 相信現(xiàn)在還運(yùn)行在1.5以下的程序應(yīng)該不是很多了,應(yīng)該影響不大, 這里只是提一下。 volatile關(guān)鍵字會(huì)禁止指令重排序優(yōu)化,屏蔽到j(luò)ava虛擬機(jī)所做的一下代碼優(yōu)化,可能會(huì)導(dǎo)致運(yùn)行效率降低, 這看起來也不是一個(gè)非常完美的實(shí)現(xiàn)方式。

餓漢式單例和懶漢式單例的比較

  • 餓漢式單例在類加載時(shí)就完成了初始化操作,因此無須考慮多線程的并發(fā)訪問問題,正是由于它一開始就進(jìn)行了實(shí)例化操作,從資源的利用和加載的時(shí)間上考慮可能比懶漢式要差一點(diǎn)
  • 懶漢式單例實(shí)現(xiàn)了延遲加載,但是在多線程并發(fā)訪問的情況下對(duì)性能還是有一定的影響。

既然餓漢式和懶漢式都存在自己的問題,有沒有一種方式能解決它們的問題呢,當(dāng)然有,看下面代碼:

通過使用靜態(tài)內(nèi)部類實(shí)現(xiàn)單例

public class StaticInnerClass {
    private StaticInnerClass(){}
    private static class InnerClass{
        private static final StaticInnerClass singleton = new StaticInnerClass() ;
    }

    public static StaticInnerClass getInstance(){
        return InnerClass.singleton ;
    }
}

調(diào)用getInstance()方法時(shí),才進(jìn)行實(shí)例的初始化操作,由于是static變量只會(huì)進(jìn)行一次初始化操作,有JVM來保證線程的安全性。 即實(shí)現(xiàn)了延遲加載,有可以保證線程的安全。

枚舉實(shí)現(xiàn)單例

//單例類
class Resource{
    public Resource(){
        System.out.println("resouce 構(gòu)造方法");
    }
}

//枚舉類生成Resource的單個(gè)實(shí)例
public enum EnumSingleton{
    INSTANCE ;

    private Resource resource ;
    private EnumSingleton(){
        System.out.println("EnumSingleton 構(gòu)造方法執(zhí)行");
        resource = new Resource() ;
    }

    public Resource getInstance(){
        return  resource ;
    }
}

//測(cè)試類及運(yùn)行結(jié)果
public class Test {
    public static void main(String[] args) {
        Resource s1 = EnumSingleton.INSTANCE.getInstance() ;
        Resource s4 = EnumSingleton.INSTANCE.getInstance() ;
        Resource s3 = EnumSingleton.INSTANCE.getInstance() ;
        Resource s2 = EnumSingleton.INSTANCE.getInstance() ;

        if(s1==s2 && s2 ==s3 && s3==s4 && s1 ==s4){
            System.out.println("同一個(gè)引用");
        }
    }
}

//運(yùn)行結(jié)果
EnumSingleton 構(gòu)造方法執(zhí)行
resouce 構(gòu)造方法
同一個(gè)引用

在枚舉類中構(gòu)造方法私有化,外部無法訪問,在訪問枚舉實(shí)例時(shí)會(huì)執(zhí)行構(gòu)造方法,實(shí)例化Resouce對(duì)象,枚舉實(shí)例都是static final類型的,只能被實(shí)例化一次。這也是effective java中推薦的方式。

單例模式的優(yōu)點(diǎn):

  • 在內(nèi)存中只有一個(gè)實(shí)例,減少了內(nèi)存的開支,無須頻繁的創(chuàng)建、銷毀
  • 減少了系統(tǒng)的性能開銷,避免了資源的多重占用

缺點(diǎn):

  • 沒有接口,擴(kuò)展相對(duì)比較困難,違背了單一職責(zé)的原則

使用場(chǎng)景:

要求一個(gè)類有且僅有一個(gè)對(duì)象,只允許有一個(gè)公共的訪問節(jié)點(diǎn)。 可以使用單例模式。


少年聽雨歌樓上,紅燭昏羅帳。  
壯年聽雨客舟中,江闊云低,斷雁叫西風(fēng)。
感謝支持!
                                        ---起個(gè)名忒難

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

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

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