徹頭徹尾理解單例模式與多線程

摘要: 
   
  本文首先概述了單例模式產生動機,揭示了單例模式的本質和應用場景。緊接着,咱們給出了單例模式在單線程環境下的兩種經典實現:餓漢式 和 懶漢式,可是餓漢式是線程安全的,而懶漢式是非線程安全的。在多線程環境下,咱們特別介紹了五種方式來在多線程環境下建立線程安全的單例,使用 synchronized方法synchronized塊靜態內部類雙重檢查模式 和 ThreadLocal 實現懶漢式單例,並總結出實現效率高且線程安全的單例所須要注意的事項。html


版權聲明:java

本文原創做者:書呆子Rico 
做者博客地址:http://blog.csdn.net/justloveyou_/數據庫


一. 單例模式概述

  單例模式(Singleton),也叫單子模式,是一種經常使用的設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候,整個系統只須要擁有一個的全局對象,這樣有利於咱們協調系統總體的行爲。好比在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,而後服務進程中的其餘對象再經過這個單例對象獲取這些配置信息,顯然,這種方式簡化了在複雜環境下的配置管理。編程

  特別地,在計算機系統中,線程池、緩存、日誌對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。事實上,這些應用都或多或少具備資源管理器的功能。例如,每臺計算機能夠有若干個打印機,但只能有一個 Printer Spooler (單例) ,以免兩個打印做業同時輸出到打印機中。再好比,每臺計算機能夠有若干通訊端口,系統應當集中 (單例) 管理這些通訊端口,以免一個通訊端口同時被兩個請求同時調用。總之,選擇單例模式就是爲了不不一致狀態,避免政出多頭。設計模式

  綜上所述,單例模式就是爲確保一個類只有一個實例,併爲整個系統提供一個全局訪問點的一種方法。緩存


二. 單例模式及其單線程環境下的經典實現

  單例模式應該是23種設計模式中最簡單的一種模式了,下面咱們從單例模式的定義、類型、結構和使用要素四個方面來介紹它。安全

一、單例模式理論基礎服務器

定義: 確保一個類只有一個實例,併爲整個系統提供一個全局訪問點 (向整個系統提供這個實例)。markdown

類型: 建立型模式多線程

結構:

                      技術分享

  特別地,爲了更好地理解上面的類圖,咱們以此爲契機,介紹一下類圖的幾個知識點:

  • 類圖分爲三部分,依次是類名、屬性、方法;
  • 以<<開頭和以>>結尾的爲註釋信息;
  • 修飾符+表明public,-表明private,#表明protected,什麼都沒有表明包可見;
  • 帶下劃線的屬性或方法表明是靜態的。

三要素:

  • 私有的構造方法;

  • 指向本身實例的私有靜態引用;

  • 以本身實例爲返回值的靜態的公有方法。


二、單線程環境下的兩種經典實現

  在介紹單線程環境中單例模式的兩種經典實現以前,咱們有必要先解釋一下 當即加載 和 延遲加載 兩個概念。

  • 當即加載 : 在類加載初始化的時候就主動建立實例;

  • 延遲加載 : 等到真正使用的時候纔去建立實例,不用時不去主動建立。

      在單線程環境下,單例模式根據實例化對象時機的不一樣,有兩種經典的實現:一種是 餓漢式單例(當即加載),一種是 懶漢式單例(延遲加載)餓漢式單例在單例類被加載時候,就實例化一個對象並交給本身的引用;而懶漢式單例只有在真正使用的時候纔會實例化一個對象並交給本身的引用。代碼示例分別以下:


餓漢式單例:

// 餓漢式單例 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; } }

  咱們從懶漢式單例能夠看到,單例實例被延遲加載,即只有在真正使用的時候纔會實例化一個對象並交給本身的引用。


  總之,從速度和反應時間角度來說,餓漢式(又稱當即加載)要好一些;從資源利用效率上說,懶漢式(又稱延遲加載)要好一些。


三、單例模式的優勢

  咱們從單例模式的定義和實現,能夠知道單例模式具備如下幾個優勢:

  • 在內存中只有一個對象,節省內存空間;

  • 避免頻繁的建立銷燬對象,能夠提升性能;

  • 避免對共享資源的多重佔用,簡化訪問;

  • 爲整個系統提供一個全局訪問點。


四、單例模式的使用場景

  因爲單例模式具備以上優勢,而且形式上比較簡單,因此是平常開發中用的比較多的一種設計模式,其核心在於爲整個系統提供一個惟一的實例,其應用場景包括但不只限於如下幾種:

  • 有狀態的工具類對象;
  • 頻繁訪問數據庫或文件的對象;

五、單例模式的注意事項

  在使用單例模式時,咱們必須使用單例類提供的公有工廠方法獲得單例對象,而不該該使用反射來建立,不然將會實例化一個新對象。此外,在多線程環境下使用單例模式時,應特別注意線程安全問題,我在下文會重點講到這一點。


三. 多線程環境下單例模式的實現

  在單線程環境下,不管是餓漢式單例仍是懶漢式單例,它們都可以正常工做。可是,在多線程環境下,情形就發生了變化:因爲餓漢式單例天生就是線程安全的,能夠直接用於多線程而不會出現問題;但懶漢式單例自己是非線程安全的,所以就會出現多個實例的狀況,與單例模式的初衷是相背離的。下面我重點闡述如下幾個問題:

  • 爲何說餓漢式單例天生就是線程安全的?

  • 傳統的懶漢式單例爲何是非線程安全的?

  • 怎麼修改傳統的懶漢式單例,使其線程變得安全?

  • 線程安全的單例的實現還有哪些,怎麼實現?

  • 雙重檢查模式、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); } }

一、爲何說餓漢式單例天生就是線程安全的?

// 餓漢式單例 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 *///:~

  咱們已經在上面提到,類加載的方式是按需加載,且只加載一次。所以,在上述單例類被加載時,就會實例化一個對象並交給本身的引用,供系統使用。換句話說,在線程訪問單例對象以前就已經建立好了。再加上,因爲一個類在整個生命週期中只會被加載一次,所以該單例類只會建立一個實例,也就是說,線程每次都只能也一定只能夠拿到這個惟一的對象。所以就說,餓漢式單例天生就是線程安全的。


二、傳統的懶漢式單例爲何是非線程安全的?

// 傳統懶漢式單例 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) {…} 語句塊的情形發生。當這種這種情形發生後,該單例類就會建立出多個實例,違背單例模式的初衷。所以,傳統的懶漢式單例是非線程安全的。


三、實現線程安全的懶漢式單例的幾種正確姿式

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 進入 getSingleton3() 方法; 
  二、因爲 singleton3 爲 null,線程 1 在 //1 處進入 synchronized 塊; 
  三、一樣因爲 singleton3 爲 null,線程 1 直接前進到 //3 處,但在構造函數執行以前,使實例成爲非 null,而且該實例是未初始化的; 
  四、線程 1 被線程 2 預佔; 
  五、線程 2 檢查實例是否爲 null。由於實例不爲 null,線程 2 獲得一個不完整(未初始化)的 Singleton 對象; 
  六、線程 2 被線程 1 預佔。 
  七、線程 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

相關文章
相關標籤/搜索