設計模式之單例模式

設計模式:不偏代碼,純偏思想,解決一類問題最行之有效的辦法。

Java中有23種設計模式。

單例模式定義

單例模式,保證系統中一個類只有一個對象實例。
要保證對象唯一,單例模式有如下特點:
1。爲避免其他對象過多建立實例對象,先禁止其他程序創建該類對象;
即將構造方法私有化;
2。 爲讓其他程序訪問該類對象,可在本類中自定義一對象;
即在類中創建一個本類對象,該對象也是私有化的;
3。爲方便其他程序對自定義對象的訪問,對外提供訪問方式;
即提供一個方法獲取該對象。

動機

爲什麼使用單例模式

對於系統中的某些類來說,只有一個實例很重要,例如,一個系統中可以存在多個打印任務,但是只能有一個正在工作的任務;一個系統只能有一個窗口管理器或文件系統;一個系統只能有一個計時工具或ID(序號)生成器。如在Windows中就只能打開一個任務管理器。如果不使用機制對窗口對象進行唯一化,將彈出多個窗口,如果這些窗口顯示的內容完全一致,則是重複對象,浪費內存資源;如果這些窗口顯示的內容不一致,則意味着在某一瞬間系統有多個狀態,與實際不符,也會給用戶帶來誤解,不知道哪一個纔是真實的狀態。因此有時確保系統中某個對象的唯一性即一個類只能有一個實例非常重要。

爲什麼不使用全局變量

1,全局變量可以實現全局訪問,但是它不能保證應用程序中只有一個實例
2,編碼規範建議少用全局變量
3,全局變量不能實現繼承

哪些類是單例模式的候選類?在Java中哪些類會成爲單例?

(1) 系統資源,如文件路徑,數據庫鏈接,系統常量等
(2)全局狀態化類,類似AutomicInteger的使用

單例模式的優點:

在內存中只有一個對象,節省內存空間。
避免頻繁的創建銷燬對象,可以提高性能。
避免對共享資源的多重佔用。
可以全局訪問。

適用場景:

由於單例模式的以上優點,所以是編程中用的比較多的一種設計模式。我總結了一下我所知道的適合使用單例模式的場景:
需要頻繁實例化然後銷燬的對象。
創建對象時耗時過多或者耗資源過多,但又經常用到的對象。
有狀態的工具類對象。
頻繁訪問數據庫或文件的對象。

實現方式

第一種方式:餓漢式

所謂的餓漢式,即在類一加載的時候就初始化對象,對於單線程來說,簡單安全,使用方便。

class Singleton{
    private Singleton(){}
    //final可有可無,有了更好,
    private static final Singleton s=new Singleton();
    public static Singleton getInstance(){
        return s;
    }
}

優點

1.線程安全;:類加載時已創建實例,不存在線程安全的問題。
2.在類加載的同時已經創建好一個靜態對象,調用時反應速度快

缺點

資源效率不高,可能getInstance()永遠不會執行到,但執行該類的其他靜態方法或者加載了該類(class.forName),那麼這個實例仍然初始化

第二種方式:懶漢式

優點

資源利用率高,不執行getInstance()就不會被實例,可以執行該類的其他靜態方法

缺點

第一次加載時不夠快,多線程使用不必要的同步開銷大
懶漢式單例模式,即對象在方法被調用時才初始化,這種方式也叫做對象的延時加載。

class Singleton{
    private Singleton(){}
    private static Singleton s;
    public static Singleton getInstance(){
        if(s==null){
            s=new Singleton();
        }
        return s;
    }
}

在多線程時會出現線程安全問題。如在if s==null處,A線程進來時,判斷s爲空,則準備new對象,在這時B線程也進來了,A對象還未new完,因此B也準備new對象,這樣就不能保證Singleton的唯一性了。

解決方法:同步鎖synchronized

//同步鎖解決線程安全問題
class Singleton{
    private Singleton(){}
    private static Singleton s;
    public static synchronized Singleton getInstance(){
        if(s==null){
            s=new Singleton();
        }
        return s;
    }
}

由於懶漢式同步鎖,每次都得判斷鎖,比較低效,因此將其變爲同步代碼塊形式,即雙重判斷加同步鎖解決線程安全

雙重判斷加同步鎖解決線程安全

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

爲什麼使用雙重判斷呢?
如果兩線程AB同時到達第一層判斷,由於此時判空都爲true,因此都會進入同步代碼塊,其中一個先進入同步,new對象,另一個阻塞等待前者釋放鎖,當前者釋放後,另一個進入,如果沒有第二層判空,則將再次new一個對象,不符合單例模式的唯一性,因此在同步代碼塊裏面也加了一層判空條件。

這種方式是絕對的線程安全的麼?!

其實不是的。這就涉及到了JVM的內存模型了。我們知道,由於硬件與操作系統之間內存訪問的差異性,產生了JMM。變量都是存放在JVM的主內存中的,變量的操作都是在線程的工作內存中,在工作內存中進行運算之後再寫回主內存的。
程序中s=new Singleton()這句話,它不是一個原子操作,其實是分爲三個JVM指令的:
memory =allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
instance =memory; //3:設置instance指向剛分配的內存地址

上面操作2依賴於操作1,但是操作3並不依賴於操作2,所以JVM是可以針對它們進行指令的優化重排序的,經過重排序後如下:

memory =allocate(); //1:分配對象的內存空間
instance =memory; //3:instance指向剛分配的內存地址,此時對象還未初始化
ctorInstance(memory); //2:初始化對象

我們可以看到,2是建立在1上的,和1有一定的依賴關係,而3是指向1的,和2之間不存在依賴關係,這就會導致變成字節碼指令時會出現指令重排,即可能是1,2,3,也可能是1,3,2。

對於1,3,2.可能在3完成之後就將其寫回主內存,導致其指向的地址不爲空,當前線程還未進行2,或者2的結果還未返回到主內存,這時另外一個線程就得到了主內存中的s,結果發現不爲空,就直接返回s.但實際上s的地址內是空的,這就會出現錯誤。因此,針對於這種情況,我們需要禁止指令重排。

重點

首先,同步塊加鎖,可以對對象實例加鎖,也可以對類對象加鎖,而單例模式中不對instance加鎖的原因在於加鎖時,instance還未初始化,如果對其加鎖,會報空指針異常。
其次,應該注意的是使用內置鎖加鎖的是Singleton.class,並不是instance,也就是說沒有在instance實現同步,那麼在這種情況下,當有兩個線程同時進行到synchronized代碼塊時,只有一個線程可以進入,然後初始化了instance,但是這僅僅只能保證的是兩個線程在訪問上的獨佔性,也就是說兩個線程在此一定是一先一後進行訪問,但是不能保證的是instance的內存可見性,原因很簡單,因爲同步的對象並不是instance,而是Singleton.class(可以保證內存可見性)。不能保證內存可見性的後果就是當第一個線程初始化instance之後,第二個線程並不能馬上看見instance被初始化,或者更準確的來說,第二個線程看到的可能只是被部分構造的instance。因此,這種造成的後果是第二個線程讀取到了錯誤的instance的狀態,有可能instance會被再次實例化。

volatile關鍵字修飾

在Java中,關鍵字volatile不僅可以保持可見性,只要一被改動就立即寫回內存,讓其他線程看到改變,還可以禁止指令重排,因此我們可以將實例定義爲volatile類型的,如下所示:

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

Volatile 變量具有 synchronized 的可見性特性,但是不具備原子特性。這種實現方式既可以實現線程安全地創建實例,而又不會對性能造成太大的影響。它只是第一次創建實例的時候同步,以後就不需要同步了,從而加快了運行速度。

有一種方式,既能實現餓漢式的特性,又能實現懶漢式的特性麼? 有!內部類實現單例模式

內部類實現單例模式

-Lazy initialization holder class模式

這個模式綜合使用了Java的類級內部類和多線程缺省同步鎖的知識,很巧妙地同時實現了延遲加載和線程安全。

相應的基礎知識

什麼是類級內部類?

  簡單點說,類級內部類指的是,有static修飾的成員式內部類。如果沒有static修飾的成員式內部類被稱爲對象級內部類。
  類級內部類相當於其外部類的static成分,它的對象與外部類對象間不存在依賴關係,因此可直接創建。而對象級內部類的實例,是綁定在外部對象實例中的。
  類級內部類中,可以定義靜態的方法。在靜態方法中只能夠引用外部類中的靜態成員方法或者成員變量。
  類級內部類相當於其外部類的成員,只有在第一次被使用的時候才被會裝載。

多線程缺省同步鎖的知識

  大家都知道,在多線程開發中,爲了解決併發問題,主要是通過使用synchronized來加互斥鎖進行同步控制。但是在某些情況中,JVM已經隱含地爲您執行了同步,這些情況下就不用自己再來進行同步控制了。這些情況包括:
  1.由靜態初始化器(在靜態字段上或static{}塊中的初始化器)初始化數據時
  2.訪問final字段時
  3.在創建線程之前創建對象時
  4.線程可以看見它將要處理的對象時

  要想很簡單地實現線程安全,可以採用靜態初始化器的方式,它可以由JVM來保證線程的安全性。比如前面的餓漢式實現方式。但是這樣一來,不是會浪費一定的空間嗎?因爲這種實現方式,會在類裝載的時候就初始化對象,不管你需不需要。

  如果現在有一種方法能夠讓類裝載的時候不去初始化對象,那不就解決問題了?一種可行的方式就是採用類級內部類,在這個類級內部類裏面去創建對象實例。這樣一來,只要不使用到這個類級內部類,那就不會創建對象實例,從而同時實現延遲加載和線程安全。

class Singleton{
    private Singleton(){}
    //類級的內部類,也就是靜態的成員式內部類,該內部類的實例與外部類的實例沒有綁定關係,而且只有被調用到時纔會裝載,從而實現了延遲加載。
    private static class SingletonHolder{
        // 靜態初始化器,由JVM來保證線程安全 
        private static Singleton s=new Singleton();
    }
    public static Singleton getInstance(){
        return SingletonHolder.s;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章