設計模式:單例模式介紹及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 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. 提供公有的靜態方法。