深刻解析單例模式

  單例模式在程序設計中很是的常見,通常來講,某些類,咱們但願在程序運行期間有且只有一個實例,緣由多是該類的建立須要消耗系統過多的資源、花費不少的時間,或者業務上客觀就要求了只能有一個實例。一個場景就是:咱們的應用程序有一些配置文件,咱們但願只在系統啓動的時候讀取這些配置文件,並將這些配置保存在內存中,之後在程序中使用這些配置文件信息的時候沒必要再從新讀取。java

定義:緩存

  因爲某種須要,要保證一個類在程序的生命週期當中只有一個實例,而且提供該實例的全局訪問方法。安全

結構:多線程

  通常包含三個要素:ide

  1.私有的靜態的實例對象 private static instance函數

  2.私有的構造函數(保證在該類外部,沒法經過new的方式來建立對象實例) private Singleton(){}性能

  3.公有的、靜態的、訪問該實例對象的方法 public static Singleton getInstance(){}測試

UML類圖:spa

分類:線程

  單例模式就實例的建立時機來劃分可分爲:懶漢式與飢漢式兩種。

  舉個平常生活中的例子:

    媽媽早上起來爲咱們作飯吃,飯快作好的時候,通常都會叫咱們起牀吃飯,這是通常的平常狀況。若是飯尚未好的時候,咱們就本身起來了(這時候媽媽尚未叫咱們起牀),這種狀況在單例模式中稱之爲飢漢式(媽媽尚未叫咱們起牀,咱們本身就起來的,就是外部尚未調用本身,本身的實例就已經建立好了)。若是飯作好了,媽媽叫咱們起牀以後,咱們才慢吞吞的起牀,這種狀況在單例模式中稱之爲懶漢式(飯都作好了,媽媽叫你起牀以後,本身才起的,能不懶漢嗎?就是外部對該類的方法發出調用以後,該實例才創建的)。

  懶漢式:顧名思義懶漢式就是應用剛啓動的時候,並不建立實例,當外部調用該類的實例或者該類實例方法的時候,才建立該類的實例。是以時間換空間。

  懶漢式的優勢:實例在被使用的時候才被建立,能夠節省系統資源,體現了延遲加載的思想

  延遲加載:通俗上將就是:一開始的時候不加載資源,一直等到立刻就要使用這個資源的時候,躲不過去了才加載,這樣能夠儘量的節省系統資源。

      懶漢式的缺點:因爲系統剛啓動時且未被外部調用時,實例沒有建立;若是一時間有多個線程同時調用LazySingleton.getLazyInstance()方法頗有可能會產生多個實例。

         也就是說下面的懶漢式在多線程下,是不能保持單例實例的惟一性的,要想保證多線程下的單例實例的惟一性得用同步,同步會致使多線程下因爲爭奪鎖資源,運行效率不高。

  飢漢式:顧名思義懶漢式就是應用剛啓動的時候,無論外部有沒有調用該類的實例方法,該類的實例就已經建立好了。以空間換時間。

  飢漢式的優勢:寫法簡單,在多線程下也能保證單例實例的惟一性,不用同步,運行效率高。

  飢漢式的缺點:在外部沒有使用到該類的時候,該類的實例就建立了,若該類實例的建立比較消耗系統資源,而且外部一直沒有調用該實例,那麼這部分的系統資源的消耗是沒有意義的。

 下面是懶漢式單例類的演示代碼:

 1 package singleton;
 2 
 3 /**
 4  * 懶漢式單例類
 5  */
 6 public class LazySingleton {
 7 
 8     //私有化構造函數,防止在該類外部經過new的形式建立實例
 9     private LazySingleton() {
10         System.out.println("生成LazySingleton實例一次!");
11     }
12 
13     //私有的、靜態的實例,設置爲私有的防止外部直接訪問該實例變量,設置爲靜態的,說明該實例是LazySingleton類型的惟一的
14     //若開始時,沒有調用訪問實例的方法,那麼實例就不會本身建立
15     private static LazySingleton lazyInstance = null;
16 
17     //公有的訪問單例實例的方法,當外部調用訪問該實例的方法時,實例才被建立
18     public static LazySingleton getLazyInstance() {
19         //若實例尚未建立,則建立實例;若實例已經被建立了,則直接返回以前建立的實例,即不會返回2個實例
20         if (lazyInstance == null) {
21             lazyInstance = new LazySingleton();
22         }
23         return lazyInstance;
24     }
25 }

下面測試類:

 1 package singleton;
 2 
 3 
 4 public class SingletonTest {
 5     public static void main(String[] args) {
 6         LazySingleton lazyInstance1 = LazySingleton.getLazyInstance();
 7         LazySingleton lazyInstance2 = LazySingleton.getLazyInstance();
 8         LazySingleton lazyInstance3 = LazySingleton.getLazyInstance();
 9     }
10 }

在上面的測試類SingletonTest 裏面,連續調用了三次LazySingleton.getLazyInstance()方法,

控制檯輸出:

生成LazySingleton實例一次!

 

下面代碼演示飢漢式單例實現:

 1 package singleton;
 2 
 3 public class NoLazySingleton {
 4 
 5     //私有化構造函數,防止在該類外部經過new的形式建立實例
 6     private NoLazySingleton(){
 7         System.out.println("建立NoLazySingleton實例一次!");
 8     }
 9 
10     //私有的、靜態的實例,設置爲私有的防止外部直接訪問該實例變量,設置爲靜態的,說明該實例是LazySingleton類型的惟一的
11     //當系統加載NoLazySingleton類文件的時候,就建立了該類的實例
12     private static NoLazySingleton instance = new NoLazySingleton();
13 
14     //公有的訪問單例實例的方法
15     public static NoLazySingleton getInstance(){
16         return instance;
17     }
18 }

測試代碼:

package singleton;

public class SingletonTest {
    public static void main(String[] args) {
        NoLazySingleton instance = NoLazySingleton.getInstance();
        NoLazySingleton instance1 = NoLazySingleton.getInstance();
        NoLazySingleton instanc2 = NoLazySingleton.getInstance();
        NoLazySingleton instanc3 = NoLazySingleton.getInstance();
    }
}

控制檯輸出:

建立NoLazySingleton實例一次!

 

上面說到了懶漢式在多線程環境下面是有問題的,下面演示這個多線程環境下頗有可能出現的問題:

 1 package singleton;
 2 
 3 /**
 4  * 懶漢式單例類
 5  */
 6 public class LazySingleton {
 7 
 8     //爲了易於模擬多線程下,懶漢式出現的問題,咱們在建立實例的構造函數裏面使當前線程暫停了50毫秒
 9     private LazySingleton() {
10         try {
11             Thread.sleep(50);
12         } catch (InterruptedException e) {
13             e.printStackTrace();
14         }
15         System.out.println("生成LazySingleton實例一次!");
16     }
17 
18     private static LazySingleton lazyInstance = null;
19 
20     public static LazySingleton getLazyInstance() {
21         if (lazyInstance == null) {
22             lazyInstance = new LazySingleton();
23         }
24         return lazyInstance;
25     }
26 }

下面是測試代碼: 咱們在測試代碼裏面 新建了10個線程,讓這10個線程同時調用LazySingleton.getLazyInstance()方法

 1 package singleton;
 2 
 3 public class SingletonTest {
 4     public static void main(String[] args) {
 5         for (int i = 0; i < 10; i++) {
 6              new Thread(){
 7                 @Override
 8                 public void run() {
 9                     LazySingleton.getLazyInstance();
10                 }
11             }.start();
12         }
13     }
14 }

結果控制檯輸出:

生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!

沒錯,你沒有看錯,控制檯輸出了10次,表示懶漢式單例模式在10個線程同時訪問的時候,建立了10個實例,這足以說明懶漢式單例在多線程下已不能保持其實例的惟一性。

那爲何多線程下懶漢式單例會失效?咱們下面分析緣由:

  咱們不說這麼多的線程,就說2個線程同時訪問上面的懶漢式單例,如今有兩個線程A和B同時訪問LazySingleton.getLazyInstance()方法。

假設A先獲得CPU的時間切片,A執行到21行處 if (lazyInstance == null) 時,因爲lazyInstance 以前並無實例化,因此lazyInstance == null爲true,在尚未執行22行實例建立的時候

此時CPU將執行時間分給了線程B,線程B執行到21行處 if (lazyInstance == null) 時,因爲lazyInstance 以前並無實例化,因此lazyInstance == null爲true,線程B繼續往下執行實例的建立過程,線程B建立完實例以後,返回。

此時CPU將時間切片分給線程A,線程A接着開始執行22行實例的建立,實例建立完以後便返回。由此看線程A和線程B分別建立了一個實例(存在2個實例了),這就致使了單例的失效。

 

那如何將懶漢式單例在多線程下正確的發揮做用呢?固然是在訪問單例實例的方法處進行同步了

下面是線程安全的懶漢式單例的實現:

 1 package singleton;
 2 
 3 
 4 public class SafeLazySingleton {
 5 
 6     private SafeLazySingleton(){
 7         System.out.println("生成SafeLazySingleton實例一次!");
 8     }
 9 
10     private static SafeLazySingleton instance = null;
11    //1.對整個訪問實例的方法進行同步
12     public synchronized static SafeLazySingleton getInstance(){
13         if (instance == null) {
14             instance = new SafeLazySingleton();
15         }
16         return instance;
17     }
    //2.對必要的代碼塊進行同步
18 public static SafeLazySingleton getInstance1(){ 19 if (instance == null) { 20 synchronized (SafeLazySingleton.class){ 21 if (instance == null) { 22 instance = new SafeLazySingleton(); 23 } 24 } 25 } 26 return instance; 27 } 28 }

 對方法同步:

上面的實現 在12行對訪問單例實例的整個方法用了synchronized 關鍵字進行方法同步,這個缺點非常明顯,就是鎖的粒度太大,不少線程同時訪問的時候致使阻塞很嚴重。

對代碼塊同步:

在18行的方法getInstance1中,只是對必要的代碼塊使用了synchronized關鍵字,注意因爲方法時static靜態的,因此監視器對象是SafeLazySingleton.class

同時咱們在19行和21行,使用了實例兩次非空判斷,一次在進入synchronized代碼塊以前,一次在進入synchronized代碼塊以後,這樣作是有深意的。

確定有小夥伴這樣想:既然19行進行了實例非空判斷了,進入synchronized代碼塊以後就沒必要再次進行非空判斷了,若是這樣作的話,會致使什麼問題?咱們來分析一下:

一樣假設咱們有兩個線程A和B,A獲取CPU時間片斷,在執行到19行時,因爲以前沒有實例化,因此instance == null 爲true,而後A得到監視器對象SafeLazySingleton.class的鎖,A進入synchronized代碼塊裏面;

與此同時線程B執行到19行,此時線程A尚未執行實例化動做,因此此時instance == null 爲true,B想進入同步塊,可是發現鎖在線程A手裏,因此B只能在同步塊外面等待。此時線程A執行實例化動做,實例化結束以後,返回該實例。

隨着線程A退出同步塊,A也釋放了鎖,線程B就得到了該鎖,若此時不進行第二次非空判斷,會致使線程B也實例化建立一個實例,而後返回本身建立的實例,這就致使了2個線程訪問建立了2個實例,致使單例失效。若進行第二次非空判斷,發現線程A已經建立了實例,instance == null已經不成立了,則直接返回線程A建立的實例,這樣就避免了單例的失效。

 

有細心的網友會發現即使去掉19行非空判斷,多線程下單例模式同樣有效:

  線程A獲取監視器對象的鎖,進入了同步代碼塊,if(instance == null) 成立,而後A建立了一個實例,而後退出同步塊,返回。這時在同步塊外面等待的線程B,獲取了鎖進入同步塊,執行if(instance == null)發現instance已經有值了再也不是空了,而後直接退出同步塊,返回。

  既然去掉19行,多線程下單例模式同樣有效,那爲何還要有進入同步塊以前的非空判斷(19行)?這應該主要是考慮到多線程下的效率問題:

  咱們知道使用synchronized關鍵字進行同步,意味着就是獨佔鎖,同一時刻只能有一個線程執行同步塊裏面的代碼,還要涉及到鎖的爭奪、釋放等問題,是很消耗資源的。單例模式,構造函數只會被調用一次。若是咱們不加19行,即不在進入同步塊以前進行非空判斷,若是以前已經有線程建立了該類的實例了,那每次的訪問該實例的方法都會進入同步塊,這會很是的耗費性能.若是進入同步塊以前加上了非空判斷,發現以前已經有線程建立了該類的實例了,那就沒必要進入同步塊了,直接返回以前建立的實例便可。這樣就基本上解決了線程同步致使的性能問題。

 

多線程下單例的優雅的解決方案:

上面的實現使用了synchronized同步塊,而且用了雙重非空校驗,這保證了懶漢式單例模式在多線程環境下的有效性,但這種實現感受仍是不夠好,不夠優雅。

下面介紹一種優雅的多線程下單例模式的實現方案:

 1 package singleton;
 2 
 3 public class GracefulSingleton {
 4     private GracefulSingleton(){
 5         System.out.println("建立GracefulSingleton實例一次!");
 6     }
 7     
     //類級的內部類,也就是靜態的成員式內部類,該內部類的實例與外部類的實例沒有綁定關係,並且只有被調用到纔會裝載,從而實現了延遲加載 8 private static class SingletonHoder{
       //靜態初始化器,由JVM來保證線程安全
9 private static GracefulSingleton instance = new GracefulSingleton(); 10 } 11 12 public static GracefulSingleton getInstance(){ 13 return SingletonHoder.instance; 14 } 15 }

上面的實現方案使用一個內部類來維護單例類的實例,當GracefulSingleton被加載的時候,其內部類並不會被初始化,因此能夠保證當GracefulSingleton被裝載到JVM的時候,不會實例化單例類,當外部調用getInstance方法的時候,纔會加載內部類SingletonHoder,從而實例化instance,同時因爲實例的創建是在類初始化時完成的,因此天生對多線程友好,getInstance方法也不須要進行同步。

 

單例模式本質上是控制單例類的實例數量只有一個,有些時候咱們可能想要某個類特定數量的實例,這種狀況能夠看作是單例模式的一種擴展狀況。好比咱們但願下面的類SingletonExtend只有三個實例,咱們能夠利用Map來緩存這些實例。

 

 1 package singleton;
 2 
 3 import java.util.HashMap;
 4 import java.util.Map;
 5 
 6 public class SingletonExtend {
 7     //裝載SingletonExtend實例的容器
 8     private static final Map<String,SingletonExtend> container = new HashMap<String, SingletonExtend>();
 9     //SingletonExtend類最多擁有的實例數量
10     private static final int MAX_NUM = 3;
11     //實例容器中元素的key的開始值
12     private static String CACHE_KEY_PRE = "cache";
13     private static int initNumber = 1;
14     private SingletonExtend(){
15         System.out.println("建立SingletonExtend實例1次!");
16     }
17 
18     //先從容器中獲取實例,若實例不存在,在建立實例,而後將建立好的實例放置在容器中
19     public static SingletonExtend getInstance(){
20         String key = CACHE_KEY_PRE+ initNumber;
21         SingletonExtend singletonExtend = container.get(key);
22         if (singletonExtend == null) {
23             singletonExtend = new SingletonExtend();
24             container.put(key,singletonExtend);
25         }
26         initNumber++;
27         //控制容器中實例的數量
28         if (initNumber > 3) {
29             initNumber = 1;
30         }
31         return singletonExtend;
32     }
33 
34     public static void main(String[] args) {
35         SingletonExtend instance = SingletonExtend.getInstance();
36         SingletonExtend instance1 = SingletonExtend.getInstance();
37         SingletonExtend instance2 = SingletonExtend.getInstance();
38         SingletonExtend instance3 = SingletonExtend.getInstance();
39         SingletonExtend instance4 = SingletonExtend.getInstance();
40         SingletonExtend instance5 = SingletonExtend.getInstance();
41         SingletonExtend instance6 = SingletonExtend.getInstance();
42         SingletonExtend instance7 = SingletonExtend.getInstance();
43         SingletonExtend instance8 = SingletonExtend.getInstance();
44         SingletonExtend instance9 = SingletonExtend.getInstance();
45         System.out.println(instance);
46         System.out.println(instance1);
47         System.out.println(instance2);
48         System.out.println(instance3);
49         System.out.println(instance4);
50         System.out.println(instance5);
51         System.out.println(instance6);
52         System.out.println(instance7);
53         System.out.println(instance8);
54         System.out.println(instance9);
55     }
56 }

 

控制檯輸出:

建立SingletonExtend實例1次!
建立SingletonExtend實例1次!
建立SingletonExtend實例1次!
singleton.SingletonExtend@3a3ee284
singleton.SingletonExtend@768965fb
singleton.SingletonExtend@36867e89
singleton.SingletonExtend@3a3ee284
singleton.SingletonExtend@768965fb
singleton.SingletonExtend@36867e89
singleton.SingletonExtend@3a3ee284
singleton.SingletonExtend@768965fb
singleton.SingletonExtend@36867e89
singleton.SingletonExtend@3a3ee284

從控制檯輸出狀況能夠看到 咱們成功的控制了SingletonExtend的實例數據只有三個

 

下面就單例模式總結一下:

咱們講了什麼是單例模式,它的結構是怎麼樣的,而且給出了單例的類圖,講了單例的分類:懶漢式和飢漢式,分別講了它們在單線程、多線程環境下的實現方式,它們的優勢和缺點,以及優雅的單例模式的實現,最後講了單例模式的擴展,小夥伴們大家清楚了嗎?

相關文章
相關標籤/搜索