摘要:
本文首先概述了單例模式產生動機,揭示了單例模式的本質和應用場景。緊接着,我們給出了單例模式在單線程環境下的兩種經典實現:餓漢式 和 懶漢式,但是餓漢式是線程安全的,而懶漢式是非線程安全的。在多線程環境下,我們特別介紹了五種方式來在多線程環境下創建線程安全的單例,使用 synchronized方法、synchronized塊、靜態內部類、雙重檢查模式 和 ThreadLocal 實現懶漢式單例,並總結出實現效率高且線程安全的單例所需要注意的事項。
版權聲明:
本文原創作者:書呆子Rico
作者博客地址:http://blog.csdn.net/justloveyou_/
一. 單例模式概述
單例模式(Singleton),也叫單子模式,是一種常用的設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候,整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行為。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然后服務進程中的其他對象再通過這個單例對象獲取這些配置信息,顯然,這種方式簡化了在復雜環境下的配置管理。
特別地,在計算機系統中,線程池、緩存、日志對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。事實上,這些應用都或多或少具有資源管理器的功能。例如,每台計算機可以有若干個打印機,但只能有一個 Printer Spooler (單例) ,以避免兩個打印作業同時輸出到打印機中。再比如,每台計算機可以有若干通信端口,系統應當集中 (單例) 管理這些通信端口,以避免一個通信端口同時被兩個請求同時調用。總之,選擇單例模式就是為了避免不一致狀態,避免政出多頭。
綜上所述,單例模式就是為確保一個類只有一個實例,並為整個系統提供一個全局訪問點的一種方法。
二. 單例模式及其單線程環境下的經典實現
單例模式應該是23種設計模式中最簡單的一種模式了,下面我們從單例模式的定義、類型、結構和使用要素四個方面來介紹它。
1、單例模式理論基礎
定義: 確保一個類只有一個實例,並為整個系統提供一個全局訪問點 (向整個系統提供這個實例)。
類型: 創建型模式
結構:

特別地,為了更好地理解上面的類圖,我們以此為契機,介紹一下類圖的幾個知識點:
- 類圖分為三部分,依次是類名、屬性、方法;
- 以<<開頭和以>>結尾的為注釋信息;
- 修飾符+代表public,-代表private,#代表protected,什么都沒有代表包可見;
- 帶下划線的屬性或方法代表是靜態的。
三要素:
-
私有的構造方法;
-
指向自己實例的私有靜態引用;
-
以自己實例為返回值的靜態的公有方法。
2、單線程環境下的兩種經典實現
在介紹單線程環境中單例模式的兩種經典實現之前,我們有必要先解釋一下 立即加載 和 延遲加載 兩個概念。
-
立即加載 : 在類加載初始化的時候就主動創建實例;
-
延遲加載 : 等到真正使用的時候才去創建實例,不用時不去主動創建。
在單線程環境下,單例模式根據實例化對象時機的不同,有兩種經典的實現:一種是 餓漢式單例(立即加載),一種是 懶漢式單例(延遲加載)。餓漢式單例在單例類被加載時候,就實例化一個對象並交給自己的引用;而懶漢式單例只有在真正使用的時候才會實例化一個對象並交給自己的引用。代碼示例分別如下:
餓漢式單例:
// 餓漢式單例 public class Singleton1 { // 指向自己實例的私有靜態引用,主動創建 private static Singleton1 singleton1 = new Singleton1(); // 私有的構造方法 private Singleton1(){} // 以自己實例為返回值的靜態的公有方法,靜態工廠方法 public static Singleton1 getSingleton1(){ return singleton1; } }
我們知道,類加載的方式是按需加載,且加載一次。。因此,在上述單例類被加載時,就會實例化一個對象並交給自己的引用,供系統使用;而且,由於這個類在整個生命周期中只會被加載一次,因此只會創建一個實例,即能夠充分保證單例。
懶漢式單例:
// 懶漢式單例 public class Singleton2 { // 指向自己實例的私有靜態引用 private static Singleton2 singleton2; // 私有的構造方法 private Singleton2(){} // 以自己實例為返回值的靜態的公有方法,靜態工廠方法 public static synchronized Singleton2 getSingleton2(){ // 被動創建,在真正需要使用時才去創建 if (singleton2 == null) { singleton2 = new Singleton2(); } return singleton2; } }
我們從懶漢式單例可以看到,單例實例被延遲加載,即只有在真正使用的時候才會實例化一個對象並交給自己的引用。
總之,從速度和反應時間角度來講,餓漢式(又稱立即加載)要好一些;從資源利用效率上說,懶漢式(又稱延遲加載)要好一些。
3、單例模式的優點
我們從單例模式的定義和實現,可以知道單例模式具有以下幾個優點:
-
在內存中只有一個對象,節省內存空間;
-
避免頻繁的創建銷毀對象,可以提高性能;
-
避免對共享資源的多重占用,簡化訪問;
-
為整個系統提供一個全局訪問點。
4、單例模式的使用場景
由於單例模式具有以上優點,並且形式上比較簡單,所以是日常開發中用的比較多的一種設計模式,其核心在於為整個系統提供一個唯一的實例,其應用場景包括但不僅限於以下幾種:
- 有狀態的工具類對象;
- 頻繁訪問數據庫或文件的對象;
5、單例模式的注意事項
在使用單例模式時,我們必須使用單例類提供的公有工廠方法得到單例對象,而不應該使用反射來創建,否則將會實例化一個新對象。此外,在多線程環境下使用單例模式時,應特別注意線程安全問題,我在下文會重點講到這一點。
三. 多線程環境下單例模式的實現
在單線程環境下,無論是餓漢式單例還是懶漢式單例,它們都能夠正常工作。但是,在多線程環境下,情形就發生了變化:由於餓漢式單例天生就是線程安全的,可以直接用於多線程而不會出現問題;但懶漢式單例本身是非線程安全的,因此就會出現多個實例的情況,與單例模式的初衷是相背離的。下面我重點闡述以下幾個問題:
-
為什么說餓漢式單例天生就是線程安全的?
-
傳統的懶漢式單例為什么是非線程安全的?
-
怎么修改傳統的懶漢式單例,使其線程變得安全?
-
線程安全的單例的實現還有哪些,怎么實現?
-
雙重檢查模式、Volatile關鍵字 在單例模式中的應用
-
ThreadLocal 在單例模式中的應用
特別地,為了能夠更好的觀察到單例模式的實現是否是線程安全的,我們提供了一個簡單的測試程序來驗證。該示例程序的判斷原理是:
開啟多個線程來分別獲取單例,然后打印它們所獲取到的單例的hashCode值。若它們獲取的單例是相同的(該單例模式的實現是線程安全的),那么它們的hashCode值一定完全一致;若它們的hashCode值不完全一致,那么獲取的單例必定不是同一個,即該單例模式的實現不是線程安全的,是多例的。注意,相應輸出結果附在每個單例模式實現示例后。
若看官對上述原理不夠了解,請移步我的博客《Java 中的 ==, equals 與 hashCode 的區別與聯系》。
public class Test { public static void main(String[] args) { Thread[] threads = new Thread[10]; for (int i = 0; i < threads.length; i++) { threads[i] = new TestThread(); } for (int i = 0; i < threads.length; i++) { threads[i].start(); } } } class TestThread extends Thread { @Override public void run() { // 對於不同單例模式的實現,只需更改相應的單例類名及其公有靜態工廠方法名即可 int hash = Singleton5.getSingleton5().hashCode(); System.out.println(hash); } }
1、為什么說餓漢式單例天生就是線程安全的?
// 餓漢式單例 public class Singleton1 { // 指向自己實例的私有靜態引用,主動創建 private static Singleton1 singleton1 = new Singleton1(); // 私有的構造方法 private Singleton1(){} // 以自己實例為返回值的靜態的公有方法,靜態工廠方法 public static Singleton1 getSingleton1(){ return singleton1; } }/* Output(完全一致): 1028355155 1028355155 1028355155 1028355155 1028355155 1028355155 1028355155 1028355155 1028355155 1028355155 *///:~
我們已經在上面提到,類加載的方式是按需加載,且只加載一次。因此,在上述單例類被加載時,就會實例化一個對象並交給自己的引用,供系統使用。換句話說,在線程訪問單例對象之前就已經創建好了。再加上,由於一個類在整個生命周期中只會被加載一次,因此該單例類只會創建一個實例,也就是說,線程每次都只能也必定只可以拿到這個唯一的對象。因此就說,餓漢式單例天生就是線程安全的。
2、傳統的懶漢式單例為什么是非線程安全的?
// 傳統懶漢式單例 public class Singleton2 { // 指向自己實例的私有靜態引用 private static Singleton2 singleton2; // 私有的構造方法 private Singleton2(){} // 以自己實例為返回值的靜態的公有方法,靜態工廠方法 public static synchronized Singleton2 getSingleton2(){ // 被動創建,在真正需要使用時才去創建 if (singleton2 == null) { singleton2 = new Singleton2(); } return singleton2; } }/* Output(不完全一致): 1084284121 2136955031 2136955031 1104499981 298825033 298825033 2136955031 482535999 298825033 2136955031 *///:~
上面發生非線程安全的一個顯著原因是,會有多個線程同時進入 if (singleton2 == null) {…} 語句塊的情形發生。當這種這種情形發生后,該單例類就會創建出多個實例,違背單例模式的初衷。因此,傳統的懶漢式單例是非線程安全的。
3、實現線程安全的懶漢式單例的幾種正確姿勢
1)、同步延遲加載 — synchronized方法
// 線程安全的懶漢式單例 public class Singleton2 { private static Singleton2 singleton2; private Singleton2(){} // 使用 synchronized 修飾,臨界資源的同步互斥訪問 public static synchronized Singleton2 getSingleton2(){ if (singleton2 == null) { singleton2 = new Singleton2(); } return singleton2; } }/* Output(完全一致): 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 *///:~
該實現與上面傳統懶漢式單例的實現唯一的差別就在於:是否使用 synchronized 修飾 getSingleton2()方法。若使用,就保證了對臨界資源的同步互斥訪問,也就保證了單例。
從執行結果上來看,問題已經解決了,但是這種實現方式的運行效率會很低,因為同步塊的作用域有點大,而且鎖的粒度有點粗。同步方法效率低,那我們考慮使用同步代碼塊來實現。
更多關於 synchronized 關鍵字 的介紹, 請移步我的博文《Java 並發:內置鎖 Synchronized》。
2)、同步延遲加載 — synchronized塊
// 線程安全的懶漢式單例 public class Singleton2 { private static Singleton2 singleton2; private Singleton2(){} public static Singleton2 getSingleton2(){ synchronized(Singleton2.class){ // 使用 synchronized 塊,臨界資源的同步互斥訪問 if (singleton2 == null) { singleton2 = new Singleton2(); } } return singleton2; } }/* Output(完全一致): 16993205 16993205 16993205 16993205 16993205 16993205 16993205 16993205 16993205 16993205 *///:~
該實現與上面synchronized方法版本實現類似,此不贅述。從執行結果上來看,問題已經解決了,但是這種實現方式的運行效率仍然比較低,事實上,和使用synchronized方法的版本相比,基本沒有任何效率上的提高。
3)、同步延遲加載 — 使用內部類實現延遲加載
// 線程安全的懶漢式單例 public class Singleton5 { // 私有內部類,按需加載,用時加載,也就是延遲加載 private static class Holder { private static Singleton5 singleton5 = new Singleton5(); } private Singleton5() { } public static Singleton5 getSingleton5() { return Holder.singleton5; } } /* Output(完全一致): 482535999 482535999 482535999 482535999 482535999 482535999 482535999 482535999 482535999 482535999 *///:~
如上述代碼所示,我們可以使用內部類實現線程安全的懶漢式單例,這種方式也是一種效率比較高的做法。至於其為什么是線程安全的,其與問題 “為什么說餓漢式單例天生就是線程安全的?” 相類似,此不贅述。
更多關於 內部類 的介紹, 請移步我的博文《 Java 內部類綜述 》。
關於使用雙重檢查、ThreaLocal實現線程安全的懶漢式單例分別見第四節和第五節。
四. 單例模式與雙重檢查(Double-Check idiom)
使用雙重檢測同步延遲加載去創建單例的做法是一個非常優秀的做法,其不但保證了單例,而且切實提高了程序運行效率。對應的代碼清單如下:
// 線程安全的懶漢式單例 public class Singleton3 { //使用volatile關鍵字防止重排序,因為 new Instance()是一個非原子操作,可能創建一個不完整的實例 private static volatile Singleton3 singleton3; private Singleton3() { } public static Singleton3 getSingleton3() { // Double-Check idiom if (singleton3 == null) { synchronized (Singleton3.class) { // 1 // 只需在第一次創建實例時才同步 if (singleton3 == null) { // 2 singleton3 = new Singleton3(); // 3 } } } return singleton3; } }/* Output(完全一致): 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 1104499981 *///:~
如上述代碼所示,為了在保證單例的前提下提高運行效率,我們需要對 singleton3 進行第二次檢查,目的是避開過多的同步(因為這里的同步只需在第一次創建實例時才同步,一旦創建成功,以后獲取實例時就不需要同步獲取鎖了)。這種做法無疑是優秀的,但是我們必須注意一點:
必須使用volatile關鍵字修飾單例引用。
那么,如果上述的實現沒有使用 volatile 修飾 singleton3,會導致什么情形發生呢? 為解釋該問題,我們分兩步來闡述:
(1)、當我們寫了 new 操作,JVM 到底會發生什么?
首先,我們要明白的是: new Singleton3() 是一個非原子操作。代碼行 singleton3 = new Singleton3(); 的執行過程可以形象地用如下3行偽代碼來表示:
memory = allocate(); //1:分配對象的內存空間 ctorInstance(memory); //2:初始化對象 singleton3 = memory; //3:使singleton3指向剛分配的內存地址
但實際上,這個過程可能發生無序寫入(指令重排序),也就是說上面的3行指令可能會被重排序導致先執行第3行后執行第2行,也就是說其真實執行順序可能是下面這種:
memory = allocate(); //1:分配對象的內存空間 singleton3 = memory; //3:使singleton3指向剛分配的內存地址 ctorInstance(memory); //2:初始化對象
這段偽代碼演示的情況不僅是可能的,而且是一些 JIT 編譯器上真實發生的現象。
(2)、重排序情景再現
了解 new 操作是非原子的並且可能發生重排序這一事實后,我們回過頭看使用 Double-Check idiom 的同步延遲加載的實現:
我們需要重新考察上述清單中的 //3 行。此行代碼創建了一個 Singleton 對象並初始化變量 singleton3 來引用此對象。這行代碼存在的問題是,在 Singleton 構造函數體執行之前,變量 singleton3 可能提前成為非 null 的,即賦值語句在對象實例化之前調用,此時別的線程將得到的是一個不完整(未初始化)的對象,會導致系統崩潰。下面是程序可能的一組執行步驟:
1、線程 1 進入 getSingleton3() 方法;
2、由於 singleton3 為 null,線程 1 在 //1 處進入 synchronized 塊;
3、同樣由於 singleton3 為 null,線程 1 直接前進到 //3 處,但在構造函數執行之前,使實例成為非 null,並且該實例是未初始化的;
4、線程 1 被線程 2 預占;
5、線程 2 檢查實例是否為 null。因為實例不為 null,線程 2 得到一個不完整(未初始化)的 Singleton 對象;
6、線程 2 被線程 1 預占。
7、線程 1 通過運行 Singleton3 對象的構造函數來完成對該對象的初始化。
顯然,一旦我們的程序在執行過程中發生了上述情形,就會造成災難性的后果,而這種安全隱患正是由於指令重排序的問題所導致的。讓人興奮地是,volatile 關鍵字正好可以完美解決了這個問題。也就是說,我們只需使用volatile關鍵字修飾單例引用就可以避免上述災難。
特別地,由於 volatile關鍵字的介紹 和 類加載及對象初始化順序 兩塊內容已經在我之前的博文中介紹過,再此只給出相關鏈接,不再贅述。
更多關於 volatile關鍵字 的介紹, 請移步我的博文《 Java 並發:volatile 關鍵字解析》。
更多關於 類加載及對象初始化順序的介紹, 請移步我的博文《 Java 繼承、多態與類的復用》。
五. 單例模式 與 ThreadLocal
借助於 ThreadLocal,我們可以實現雙重檢查模式的變體。我們將臨界資源線程局部化,具體到本例就是將雙重檢測的第一層檢測條件 if (instance == null) 轉換為 線程局部范圍內的操作 。這里的 ThreadLocal 也只是用作標識而已,用來標識每個線程是否已訪問過:如果訪問過,則不再需要走同步塊,這樣就提高了一定的效率。對應的代碼清單如下:
// 線程安全的懶漢式單例 public class Singleton4 { // ThreadLocal 線程局部變量 private static ThreadLocal<Singleton4> threadLocal = new ThreadLocal<Singleton4>(); private static Singleton4 singleton4 = null; // 不需要是 private Singleton4(){} public static Singleton4 getSingleton4(){ if (threadLocal.get() == null) { // 第一次檢查:該線程是否第一次訪問 createSingleton4(); } return singleton4; } public static void createSingleton4(){ synchronized (Singleton4.class) { if (singleton4 == null) { // 第二次檢查:該單例是否被創建 singleton4 = new Singleton4(); // 只執行一次 } } threadLocal.set(singleton4); // 將單例放入當前線程的局部變量中 } }/* Output(完全一致): 1028355155 1028355155 1028355155 1028355155 1028355155 1028355155 1028355155 1028355155 1028355155 1028355155 *///:~
借助於 ThreadLocal,我們也可以實現線程安全的懶漢式單例。但與直接雙重檢查模式使用,本實現在效率上還不如后者。
更多關於 ThreadLocal 的介紹, 請移步我的博文《 Java 並發:深入理解 ThreadLocal》。
六. 小結
本文首先介紹了單例模式的定義和結構,並給出了其在單線程和多線程環境下的幾種經典實現。特別地,我們知道,傳統的餓漢式單例無論在單線程還是多線程環境下都是線程安全的,但是傳統的懶漢式單例在多線程環境下是非線程安全的。為此,我們特別介紹了五種方式來在多線程環境下創建線程安全的單例,包括:
-
使用synchronized方法實現懶漢式單例;
-
使用synchronized塊實現懶漢式單例;
-
使用靜態內部類實現懶漢式單例;
-
使用雙重檢查模式實現懶漢式單例;
-
使用ThreadLocal實現懶漢式單例;
當然,實現懶漢式單例還有其他方式。但是,這五種是比較經典的實現,也是我們應該掌握的幾種實現方式。從這五種實現中,我們可以總結出,要想實現效率高的線程安全的單例,我們必須注意以下兩點:
-
盡量減少同步塊的作用域;
-
盡量使用細粒度的鎖。
七. 更多
本文涉及內容比較廣,涉及到 hashcode、synchronized 關鍵字、內部類、 類加載及對象初始化順序、volatile關鍵字 和 ThreadLocal 等知識點,這些知識點在我之前的博文中均專門總結過,現附上相關鏈接,感興趣的朋友可以移步到相關博文進行查看。
更多關於 hashCode 與相等 的介紹,請移步我的博客《Java 中的 ==, equals 與 hashCode 的區別與聯系》。
更多關於 synchronized 關鍵字 的介紹, 請移步我的博文《Java 並發:內置鎖 Synchronized》。
更多關於 內部類 的介紹, 請移步我的博文《 Java 內部類綜述 》
更多關於 volatile關鍵字 的介紹, 請移步我的博文《 Java 並發:volatile 關鍵字解析》。
更多關於 類加載及對象初始化順序的介紹, 請移步我的博文《 Java 繼承、多態與類的復用》。
更多關於 ThreadLocal 的介紹, 請移步我的博文《 Java 並發:深入理解 ThreadLocal》。
此外,
更多關於 Java SE 進階 方面的內容,請關注我的專欄 《Java SE 進階之路》。本專欄主要研究Java基礎知識、Java源碼和設計模式,從初級到高級不斷總結、剖析各知識點的內在邏輯,貫穿、覆蓋整個Java知識面,在一步步完善、提高把自己的同時,把對Java的所學所思分享給大家。萬丈高樓平地起,基礎決定你的上限,讓我們攜手一起勇攀Java之巔…
更多關於 Java 並發編程 方面的內容,請關注我的專欄 《Java 並發編程學習筆記》。本專欄全面記錄了Java並發編程的相關知識,並結合操作系統、Java內存模型和相關源碼對並發編程的原理、技術、設計、底層實現進行深入分析和總結,並持續跟進並發相關技術。
引用
Java 中的雙重檢查(Double-Check)
單例模式與雙重檢測
用happen-before規則重新審視DCL
JAVA設計模式之單例模式
23種設計模式(1):單例模式
轉載自:http://www.mamicode.com/info-detail-1728587.html
