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

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


# 一、餓漢式(靜態常量)
這種餓漢式的單例模式構造的步驟如下: 1. 構造器私有化;(防止用new來得到物件例項) 2. 類的內部建立物件;(因為1,所以2) 3. 向外暴露一個靜態的公共方法;(getInstance) 示例: ```java 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方法然後類裝載,是沒問題的,但是導致類裝載的原因有很多,可能有其他的方式或者靜態方法導致類裝載) **總結:** 如果確定會用到他,這種寫**是沒問題的**,但是儘量避免記憶體浪費。 # 二、餓漢式(靜態程式碼塊)
和上一種用靜態常量的方法類似,是把建立例項的過程放在靜態程式碼塊裡。 ```java class Singleton{ //1同樣私有化構造方法 private Singleton(){ } //2建立物件例項 private static Singleton instance; //在靜態程式碼塊裡進行單例物件的建立 static { instance = new Singleton(); } //3提供靜態方法返回例項物件 public static Singleton getInstance() { return instance; } } ``` **優缺點**:和上一種靜態常量的方式一樣; **原因**:實現本來就是和上面的一樣,因為類裝載的時候一樣馬上會執行靜態程式碼塊中的程式碼。 # 三、懶漢式(執行緒不安全)
上面的兩種餓漢式,都是一開始類載入的時候就建立了例項,可能會造成記憶體浪費。 懶漢式的寫法如下: ```java 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 的執行可能不同步,所以解決的方式也很簡單。 ```java 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 來讓他同步,是不是就能解決效率問題呢? ```java 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,也就是兩次判斷。 ```java 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中靜態類只有一種,那就是靜態內部類。 ```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變數。 ```java 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. 提供公有的靜態方法。