單例模式是非常常見的設計模式,其含義也很簡單,一個類給外部提供一個唯一的實例。下文所有的代碼均在 "github" 源碼整個項目不僅僅有設計模式,還有其他JavaSE知識點,歡迎Star,Fork 單例模式的UML圖 單例模式的關鍵點 通過上面的UML圖,我們可以看出單例模式的特點如下: 1. 構造 ...
單例模式是非常常見的設計模式,其含義也很簡單,一個類給外部提供一個唯一的實例。下文所有的代碼均在github
源碼整個項目不僅僅有設計模式,還有其他JavaSE知識點,歡迎Star,Fork
單例模式的UML圖
單例模式的關鍵點
通過上面的UML圖,我們可以看出單例模式的特點如下:
- 構造器是私有的,不允許外部的類調用構造器
- 提供一個供外部訪問的方法,該方法返回單例類的實例
如何實現單例模式
上面已經給出了單例模式的關鍵點,我們的實現只需要滿足上面2點即可。但是正因為單例模式的實現方式比較寬鬆,所以不同的實現方式會有不同的問題。我們可以對單例模式的實現做一下分類,看一看有哪些不同的實現方式。
- 根據單例對象的創建時機不同,可以分為餓漢模式和懶漢模式。餓漢是指在類載入的時候,就創建了對象。但是創建對象有時比較消耗資源,會造成類載入很慢,但是優點是獲取對象的速度很快,因為早已經創建好了嘛。懶漢就是相對餓漢而言,在需要返回單例對象的時候,在創建對象,類載入的時候,並不初始化,好處與缺點也不言而喻
- 根據是否實現線程安全,可以分為普通的懶漢模式這種線程不安全的寫法,和餓漢模式,雙重檢查鎖的懶漢模式,以及通過靜態內部類或者枚舉類等實現的線程安全的寫法。
一個線程不安全的單例模式
public class SimpleSingleton {
private static SimpleSingleton simpleSingleton;
private SimpleSingleton(){
}
public static SimpleSingleton getInstance(){
if (simpleSingleton == null) {
simpleSingleton = new SimpleSingleton();
}
return simpleSingleton;
}
}
首先,我們可以看出這是一個懶漢模式的實現。因為只有在getInstance的時候,才會真正創建單例的對象。但是為什麼他是線程不安全的呢,是因為可能會有2個線程同時進入if (simpleSingleton == null)
的判斷,就是同時創建了simpleSingleton對象。
DCL懶漢模式
上面的方法可以看出是存線上程不安全的問題的,我們可以用同步關鍵字synchronized
來實現線程安全。我們先逐步分析,先用synchronized
來改寫上面的懶漢模式,代碼如下:
public class DCLSingleton {
private static DCLSingleton singleton;
private DCLSingleton(){
}
public synchronized static DClSingleton getSingleton(){
if (singleton == null) {
singleton = new DCLSingleton();
}
return singleton;
}
}
這樣,就有效的保證了不會有兩個線程同時執行該方法,但這個效率也太低了吧。因為在創建實例之後,每次得到實例對象,還是需要進行同步,synchronized
的同步保證代價是比較大的,因此可以在此基礎上進行改造。在已經創建好之後,就不需要同步了,我們可以改成如下的形式:
public static DCLSingleton getSingleton(){
if (singleton == null) {
synchronized (DCLSingleton.class) {
if (singleton == null) {
singleton = new DCLSingleton();
}
}
}
return singleton;
}
其他代碼不變,只看這個方法。該方法的兩重if (singleton == null)
可以有效地保證線程安全。比如,當兩個線程同時進入該方法的時候,第一個if,兩者都是進入,下麵的代碼,但是碰到同步代碼塊,只能有一個先進入,進入的時候,繼續判斷,再次判斷為空,才會真正創建對象。如果不進行,第二個判斷,那些對於第一個進入的線程而言,確實創建了對象,但是第二個線程,他緊接著也會執行創建對象的操作,因為不知道第一個線程已經創建成功。因此,需要兩次判空。
但是真的就如此簡單的保證了線程安全嗎?我們仔細分析一下這個過程,singleton = new DCLSingleton();
這個代碼實際上是3個操作。
- 給DCLSingleton實例分配記憶體
- 調用DCLSingleton()的構造函數,初始化成員欄位
- 將singleton對象指向分配的記憶體空間。
在JDK1.5以前,上面的3個執行順序是不固定的,有可能是1-2-3,或者1-3-2。如果是1-3-2,則在第一個線程執行完第三步以後,第二個線程立即執行,但還沒有真正的進行初始化,所以就會使用的時候出錯。在JDK1.5以後,我們可以用volatile
關鍵字來保證該1-2-3的順序執行。所以,除了getSingleton()
方法要改成上面的樣子以外,還需要對private static DCLSingleton singleton;
改寫成private static volatile DCLSingleton singleton;
這樣,就真正保證了線程同步的懶漢寫法的單例模式。
餓漢寫法
餓漢寫法有很多變形,但無論是哪一種變形,都能保證線程安全,因為餓漢寫法是在類載入的時候,就完成了對象的初始化,類載入保證了他們天生是線程安全的。下麵給出常見的2中餓漢寫法
public class HungrySingleton {
private static final HungrySingleton singleton = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getSingleton(){
return singleton;
}
}
public class HungrySingleton {
private static final HungrySingleton singleton = new HungrySingleton();
private HungrySingleton(){
}
// public static HungrySingleton getSingleton(){
// return singleton;
// }
}
這兩種對初始化單例的對象上面,都是一致的, 通過final來保證對象的唯一。不同的是,調用單例對象的方式,第一種是通過getSingleton()
,第二種是通過類.類變數
的形式。
靜態內部類實現單例模式
雙重檢查鎖(DCL)實現單例模式,雖然解決了線程不安全的問題,以及保證了資源的懶載入,在需要的時候,才會進行實例化的操作。但是在某些情況下(比如JDK低於1.5)會出現DCL失效,所以有一種很簡潔且依舊是懶載入的方法實現單例模式。寫法如下:
public class StaticSingleton {
private StaticSingleton(){
}
public static final StaticSingleton getInstance(){
return Holder.singleton;
}
private static class Holder{
private static final StaticSingleton singleton = new StaticSingleton();
}
}
通過靜態內部類的形式,實現單例類的初始化,其特性同樣是通過ClassLoader來保證其單例對象的唯一,但是這是懶載入的,因為只有在Holder類被調用的時候,即getInstance
調用的時候,才會載入Holder類從而實現創建對象。
枚舉類實現單例模式
直接看代碼:
public enum EnumSingleton {
SINGLETON;
public void doSometings(){
}
}
使用的時候,直接通過EnumSingleton.SINGLETON.doSomethings()
。枚舉類天生特性是保證不會有兩個實例,並且只有在第一次訪問的時候才會被實例化,是懶載入的情況。
真的不會再次創建新的對象嗎?
在常規調用單例類的getInstance()
方法的情況下,使用線程安全的寫法確實不會創建新的對象,但是Java提供了很多奇特的技巧和使用,下麵這些使用會破壞掉常規的單例。
- 反序列化
- 反射
- 克隆
- 分散式環境下,多個類載入器
在除了枚舉實現單例模式的方法以外,其餘所有方法碰到上述四種情況,都會重新創建對象。原因如下:
- 反序列化會調用一個特殊的readResolve()方法來創建新的對象。我們可以重寫該方法,讓他返回原來的instance,而不是重新創建一個。
- 反射會得到私有的構造函數,只能在構造函數中加一個判斷,如果對象不為null,則扔出一個運行時異常,如果不這樣,只有枚舉能解決,因為枚舉自帶的特性。
- 克隆,因為直接拷貝的記憶體空間的內容,所以只有自己重寫單例類的clone方法,如果不這樣,也只有枚舉能解決,因為枚舉沒有克隆方法。
- 多分散式環境,因為我們上述很多種單例的寫法,都是依賴於類載入器的特性,但是static的作用只負責到類載入器,所以當工程中存在多個類載入器的時候,就會創建多個實例,這種通常就需要第三方庫來解決。
什麼時候用單例模式,用哪一種寫法的單例模式
單例模式有兩種比較適合的使用場景。
第一種是創建某個對象,需要的代價比較大,為了避免頻繁的創建和銷毀對象從而引起的對資源的浪費,會考慮使用單例模式。
第二種是這個對象必須只有一個,有多個會造成不可預估的錯誤,或者程式的混亂,比如只會有一個序號生成器,一個緩存等等。
針對使用的單例模式,如果需要理解的載入資源,就是用餓漢寫法,在Android應用中,很多對象需要在啟動的時候,立即就使用,比如啟動時,需要拉取相機配置的類管理縮略圖的cache類等等。如果不是立即需要,或者不是貫穿應用始終的,就不需要使用餓漢寫法,可以考慮懶漢寫法用(DCL或者靜態內部類實現)這兩種在一般情況下都不會出現問題。