設計模式:單例模式介紹及8種寫法(餓漢式、懶漢式、Double-Check、靜態內部類、枚舉)



一、餓漢式(靜態常量)


這種餓漢式的單例模式構造的步驟如下:

  1. 構造器私有化;(防止用new來得到對象實例)
  2. 類的內部創建對象;(因為1,所以2)
  3. 向外暴露一個靜態的公共方法;(getInstance)

示例:

class Singleton{
    //1私有化構造方法
    private Singleton(){

    }
    //2創建對象實例
    private final static Singleton instance = new Singleton();
    //3對外提供公有靜態方法
    public static Singleton getInstance(){
        return instance;
    }
}

這樣的話,獲取對象就不能通過 new 的方式,而要通過 Singleton.getInstance();並且多次獲取到的都是同一個對象。

使用靜態常量的餓漢式寫法實現的單例模式的優缺點:

優點:

簡單,類裝載的時候就完成了實例化,避免了多線程同步的問題。

缺點:

類裝載的時候完成實例化,沒有達到 Lazy Loading (懶加載)的效果,如果從始至終都沒用過這個實例呢?那就會造成內存的浪費。(大多數的時候,調用getInstance方法然后類裝載,是沒問題的,但是導致類裝載的原因有很多,可能有其他的方式或者靜態方法導致類裝載)

總結:

如果確定會用到他,這種寫是沒問題的,但是盡量避免內存浪費。

二、餓漢式(靜態代碼塊)


和上一種用靜態常量的方法類似,是把創建實例的過程放在靜態代碼塊里。

class Singleton{
    //1同樣私有化構造方法
    private Singleton(){

    }
    //2創建對象實例
    private static Singleton instance;
    //在靜態代碼塊里進行單例對象的創建
    static {
        instance = new Singleton();
    }
    //3提供靜態方法返回實例對象
    public static Singleton getInstance() {
        return instance;
    }
}

優缺點:和上一種靜態常量的方式一樣;

原因:實現本來就是和上面的一樣,因為類裝載的時候一樣馬上會執行靜態代碼塊中的代碼。

三、懶漢式(線程不安全)


上面的兩種餓漢式,都是一開始類加載的時候就創建了實例,可能會造成內存浪費。

懶漢式的寫法如下:

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

    }
    //提供靜態公有方法,使用的時候才創建instance
    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return  instance;
    }
}

也就是說,同樣是 1) 私有構造器;2) 類的內部創建實例;3) 向外暴露獲取實例方法。這三個步驟。

但是懶漢式的寫法,將創建的代碼放在了 getInstance 里,並且只有第一次的時候會創建,這樣的話,類加載的過程就不會創建實例,同時也保證了創建只會有一次。

優點:

起到了Lazy Loading 的作用

缺點:

但是只能在單線程下使用。如果一個線程進入了 if 判斷,但是沒來得及向下執行的時候,另一個線程也通過了這個 if 語句,這時候就會產生多個實例,所以多線程環境下不能使用這種方式。

結論:

實際開發不要用這種方式。

四、懶漢式(線程安全,同步方法)


因為上面說了主要的問題,就在於 if 的執行可能不同步,所以解決的方式也很簡單。

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

    }
    //使用的時候才創建instance,同時加入synchronized同步代碼,解決線程不安全問題
    public static synchronized Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return  instance;
    }
}

只要在獲取實例的靜態方法上加上 synchronized 關鍵字,同步機制放在getInstance方法層面,就 ok。

優點:

保留了單例的性質的情況下,解決了線程不安全的問題

缺點:

效率太差了,每個線程想要獲得類的實例的時候都調用 getInstance 方法,就要進行同步。
然而這個方法本身執行一次實例化代碼就夠了,后面的想要獲得實例,就應該直接 return ,而不是進行同步。

結論:

實際開發仍然不推薦

五、懶漢式(同步代碼塊)


這種寫法是基於對上一種的思考,既然在方法層面效率太差,那直接在實例化的語句上加 synchronized 來讓他同步,是不是就能解決效率問題呢?

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

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

事實上,這種方法,讓 synchronized 關鍵字放入方法體里,又會導致可能別的線程同樣進入 if 語句,回到了第三種的問題,所以來不及同步就會產生線程不安全的問題。

結論:不可用

六、 雙重檢查Double Checked Locking


在下面的實例化過程里采用 double check locking,也就是兩次判斷。

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

    }
    //雙重檢查
    public static Singleton getInstance(){
        //第一次檢查
        if(instance == null){
            synchronized (Singleton.class){
                //第二次檢查
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return  instance;
    }
}

4 的懶漢式同步方法寫法里,getInstance方法是用了synchronized修飾符,所以雖然解決了 lazy loading 的問題,線程也安全,但是同步起來會很慢。

而 5 的懶漢式同步代碼塊寫法,將 synchronized 修飾符加到內部的代碼塊部分,又會導致線程安全直接失效,因為可能大家都同時進入了 getInstance 方法。

所以double - checked - locking 將兩者相結合。

可是這樣的 double - checked - locking 還不能保證線程安全,原因涉及到多線程里的知識點,指令重排序:(待更新鏈接)

看起來只有一句的 instance = new Singleton(); 實際上是分為三個步驟的:

1). 申請一塊內存空間,用來裝 new 出來的對象;
2). 初始化對象信息;
3). 返回對象地址,建立連接。

而這些指令可能由於重排,先執行了第 3 步,而第 2 步由於耗時過多還在進行,此時另一個線程執行,雖然判斷出了 instance 不為空,可是他直接返回的對象是return instance,這塊地址的內容是不對的,可能是空指針,可能是錯誤的數據。

解決這個終極問題的方式是,使用 volatile 關鍵字,讓修改值立即更新到主存。

class Singleton{
    private static volatile Singleton instance;
    private Singleton(){

    }
    //雙重檢查
    public static Singleton getInstance(){
        //第一次檢查
        if(instance == null){
            synchronized (Singleton.class){
                //第二次檢查
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return  instance;
    }
}

優點

double-check是多線程開發里經常用到的,滿足了我們需要的線程安全&&避免反復進行同步的效率差&&lazy loading。

結論:推薦使用。

七、靜態內部類


靜態內部類:用static修飾的內部類,稱為靜態內部類,完全屬於外部類本身,不屬於外部類某一個對象,外部類不可以定義為靜態類,Java中靜態類只有一種,那就是靜態內部類。

class Singleton{
    //構造器私有化
    private Singleton(){

    }
    //一個靜態內部類,里面有一個靜態屬性,就是實例
    private static class SingletonInstance{
        private static final Singleton instance = new Singleton();
    }
    //靜態的公有方法
    public static Singleton getInstance(){
        return SingletonInstance.instance;
    }
}

核心:

  1. 靜態內部類在外部類裝載的時候並不會執行,也就是滿足了 lazy loading;
  2. 調用getInstance的時候會取屬性,此時才加載靜態內部類,而 jvm 底層的類裝載機制是線程安全的,所以利用 jvm 達到了我們要的線程安全;
  3. 類的靜態屬性保證了實例化也只會進行一次,滿足單例。

結論:推薦。

八、枚舉


將單例的類寫成枚舉類型,直接只有一個Instance變量。

enum Singleton{
    instance;
    public void sayOk(){
        System.out.println("ok");
    }
}

調用的時候也不用new,直接用Singleton.instance,拿到這個屬性。(一般INSTANCE寫成大寫)

優點:

滿足單例模式要的特點,同時還能夠避免反序列化重新創建新的對象。
這種方法是effective java作者提供的方式。

結論:推薦。

九、總結


單例模式使用的場景是

需要頻繁創建和銷毀的對象、創建對象耗時過多或耗資源太多(重型對象)、工具類對象、頻繁訪問數據庫或者文件的對象(數據源、session工廠等),都應用單例模式去實現。

因為單例模式保證了系統內存中只存在該類的一個對象,所以能節省資源,提高性能,那么對外來說,單例的類都不能再通過 new 去創建了,而是采用類提供的獲取實例的方法。

上面的八種寫法里面:餓漢式兩種基本是一樣的寫法,懶漢式三種都有問題,以上物種的改進就是雙重檢查,另辟蹊徑的是靜態內部類和枚舉。

所以,單例模式推薦的方式有四種:

  1. 餓漢式可用(雖然內存可能會浪費);
  2. 雙重檢查;
  3. 靜態內部類;
  4. 枚舉。

十、單例模式在JDK里的應用


Runtime類就是一個單例模式的類,並且可以看到,他是采用我們所說的第一種方式,即餓漢式(靜態常量的方式)

  1. 私有構造器;
  2. 靜態常量,類的內部直接將類實例化;
  3. 提供公有的靜態方法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM