設計模式之單例模式
單例模式是設計模式中相對是來說最容易理解的設計模式,但其實大多數人只是知道單例模式的一些簡單場景的使用和實現。比如餓漢單例、懶漢單例等。實際上,在很多場景下,這些實現方式有很多問題。那麼在不同場景下我們應該如何選擇單例的實現呢。這次我們會由淺入深的學習,徹底的來認識一下單例模式。
一、單例模式的定義
確保某一個類只有一個例項,而且自行例項化並向整個系統提供這個例項。
二、UML類圖
它的類圖很簡單,就是client使用這個單例類就行了。
需要注意的是單例類的構造方法是私有的。

單例UML.png
三、使用場景
它的使用場景也很明確。
確保某個類只要一個物件的場景,從而避免產生多個物件消耗過多的資源。
從定義、UML類圖和使用場景都透露出兩個字--簡單。
可它的背後真的只是那麼簡單嗎?哈哈, 表明簡單的東西背後往往是不簡單的。
嗯哼,不信,那就先放一張圖來震下場子

單例模式.png
這就是我這篇文章要說的核心內容了。
四、不同場景下的實現
1.說實現之前,我們先要看看實現單例模式的關鍵點
(1)構造方法不對外開放。這個很容易理解,如果開放了那麼和話,外面隨便new一下就實現了一個物件,還單例個毛線呢。
(2)確保單例類的物件只有一個,尤其是在多執行緒環境下。這個後面會詳細介紹。
(3)確保單例類的物件在反序列化時不會重新構建物件。
那麼下面我一一介紹各種實現方法和它們的優缺點
2.最簡單的實現方法--餓漢單例
餓漢,顧名思義,非常飢餓的漢子,所以一定義這個物件就給例項化了。
public class HungerSingleton { private static HungerSingleton singleton=new HungerSingleton(); private HungerSingleton() {} public static HungerSingleton getSingleton() { return singleton; } }
實現非常簡單,通過一個靜態方法返回一個靜態物件。
但是缺點也很明顯,如果我不需要這個物件的話,它也會建立物件,浪費空間資源和影響時間效能。
3.給飢餓的漢子一張床--懶漢單例
給了飢餓的漢子一張床後會忘記飢餓,但它會變懶。也就不急著例項化物件了,就是所謂的懶載入。
public class LazySingleton { private LazySingleton(){}; private static LazySingleton singleton=null; public staticsynchronized LazySingleton getSingleton() { if(singleton==null){ singleton=new LazySingleton(); } return singleton; } }
仔細一看我們發現這個getSingleton方法加了個synchronize關鍵字。這個就是我們之前說的” 保證在多執行緒下物件唯一 “的方法。
咋一看很完美,但還是有一個很大的問題就是:
我們知道synchronize同步是比較消耗效能的,而且每次呼叫getSingleton方法都會進行同步,這回造成不必要的效能開銷。
4.懶漢單例的優化--DCL單例
針對上面的懶漢單例的缺點,出現了DCL(Double Check Lock)模式。其實解決的方式很簡單了,只需要在外層加一個判空就OK了。
這樣就整個程式就有了兩次判空,第一次的目的是為了避免多餘的同步操作。第二次是為了物件例項化的唯一性。
public class DCLSingleton { private DCLSingleton(){}; private static DCLSingleton singleton=null; private static DCLSingleton getSingleton(){ if (singleton==null) { synchronized (DCLSingleton.class) { if(singleton==null) singleton=new DCLSingleton(); } } return singleton; } }
這裡我們考慮到了多執行緒的情景又考慮到效能的問題。看起來就完美了。其實不深入研究下去,這的確是完美的單例寫法。那麼還有什麼不完美的地方呢?
就是這句話
singleton=new DCLSingleton();
這個語句他不是一個原子操作。
也就是說
它表明上只有一步其實有三步操作
(1)給DCLSingleton的例項分配記憶體
(2)呼叫Singleton()的構造方法,初始化成員欄位
(3)將singleton物件指向分配的記憶體空間
由於java編譯器允許處理器亂序執行,以及jdk1.5之前的java記憶體模型(JMM)中暫存器、cache到主記憶體回寫順序的規定。所以不能保證上面三步執行的順序。很有可能會按照(1)-(3)-(2)的順序
這就會有一個問題,高併發的情況下,當A執行緒執行到(3),被切換到了B執行緒,那麼B執行緒就會直接取走了singleton物件,但是該物件因為沒有執行第二步,就沒有真正的例項化。所以就會被B拿去使用導致出錯,發生DCL失效。
那腫麼辦呀?這個我們沒法控制啊。
幸運的是,在jdk1.5的時候,出現了一個 volatile關鍵字 。當我們的jdk版本大於等於1.5的時候,只要在定義的時候加上這個volatile就能夠解決了問題了。
private static volatile DCLSingleton singleton=null;
雖然有點小瑕疵但是DCL的優點還是多的,大多數情景下可以保證單例物件的唯一性。但如果你的併發場景複雜或者版本低於jdk1.5的時候,可能需要一個更好的實現方式了。
5.《java併發程式設計實踐》推薦--靜態內部類單例
java併發程式設計實踐書中並不贊同DCL實現方式,因為這種方式還是會出現DCL失效。它建議使用靜態內部類實現
public class InternalClassSingleton { private InternalClassSingleton(){}; public static InternalClassSingleton getSingleton(){ return SingletonHolder.singleton; } private static class SingletonHolder{ private static final InternalClassSingleton singleton=new InternalClassSingleton(); } }
這種方式也是很簡潔的,它只會在第一次呼叫getSingleton()的時候才會讓singleton被初始化。因此第一次載入getSingleton的時候會導致虛擬機器載入SingletonHolder類,這種方式不僅能夠保證單例物件的唯一性,同時也延遲了單例的例項化。推薦使用的單例模式實現方式。
6.最簡單的單例實現--列舉單例
我們都知道列舉和普通的java類是一樣的,不僅有欄位還有自己的方法。重要的是預設列舉例項的建立是執行緒安全的,並且在任何情況下它只有一個單例。
public class EnumSingleton { private EnumSingleton(){}; enum Enum{ INSTANCE; public static Enum getInstance(){ return INSTANCE; } } }
實在是簡單,看著還舒服,重點不需要考慮多執行緒的場景問題。美滋滋~~
除了上面說到,其實還有其他的方式實現,比如利用容器等等。但上面的方式足以應對各種的場景了,我們只需要根據不同的場景和自己的喜好來選擇合適的就好了。