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

Life_Goes_On發表於2020-08-11

一、餓漢式(靜態常量)


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

  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 Check


使用 volatile 關鍵字,讓修改值立即更新到主存,相當於輕量級的synchronized。

然後在下面的例項化過程裡採用 double check,也就是兩次判斷。

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;
    }
}

可以回想一下:

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

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

所以雙檢查的方法,仍然採用 5 的寫法,將程式碼塊用 synchronized 修飾符修飾,同時,在這個內部,再加上第二重檢查,這樣,執行緒安全的同時,保證了後面的執行緒會先進行 if 的判斷而不進入程式碼塊,這樣就同時達到了效率的提升

優點

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. 提供公有的靜態方法。
設計模式:單例模式介紹及8種寫法(餓漢式、懶漢式、Double-Check、靜態內部類、列舉)

相關文章