java設計模式之單例模式(Singleton)

   利用元旦小假期,參考了幾篇單例模式介紹的文章,而後本身下午對java設計模式中的單例模式作了一下簡單的總結,主要是代碼介紹。java

      單例模式,在實際項目開發中運用普遍,好比數據庫鏈接池,實際上,配置信息類、管理類、控制類、門面類、代理類一般被設計爲單例類。像Java的Struts、spring框架,.Net的Spring.Net框架,以及PHP的Zend框架都大量使用單例模式。spring

  那麼,接下來,我將如下面5點來對單例模式做一下介紹:數據庫

  1.單例模式的定義設計模式

  2.單例模式的特色安全

  3.爲何要使用單例模式?網絡

  4.單例模式的5種不一樣寫法及其總結多線程

  5.拓展--如何防止Java反射機制對單例類的攻擊?併發

1.單例模式的定義

    單例模式(Singleton)是一種建立型模式,指某個類採用Singleton模式,則在這個類被建立後,只可能產生一個實例供外部訪問,而且提供一個全局的訪問點。框架

核心知識點:
 (1)將採用單例模式的類的構造方法私有化(採用private修飾);
 (2)在其內部產生該類的實例化對象,並將其封裝成private static類型
 (3)定義一個靜態方法返回該類的實例。
函數

2.單例模式的特色

  - 單例類只能有一個實例;
  - 單例類必須本身建立本身的惟一實例;
  - 單例類必須給全部其餘對象提供這一實例。

3.爲何要使用單例模式?

  根據單例模式的定義和特色,咱們會對單例模式有了初步認識,那麼由特色出發,單例模式在項目中的做用就顯而易見了。

  (1)控制資源的使用,經過線程同步來控制資源的併發訪問
  (2)控制實例產生的數量,到達節約資源的目的;
  (3)做爲通訊媒介使用,也就是數據共享,他能夠在不創建直接關聯的條件下,讓多個不相關的兩個線程或者進程實現通訊。
  好比:
  數據庫鏈接池的設計通常採用單例模式,數據庫鏈接是一種數據資源。項目中使用數據庫鏈接池,主要是節省打開或者關閉數據庫所引發的效率損耗。固然,使用數據庫鏈接池能夠屏蔽不一樣數據數據庫之間的差別,實現系統對數據庫的低度耦合,也能夠被多個系統同時使用,具備高科複用性,還能方便對數據庫鏈接的管理等。
  實際上,配置信息類、管理類、控制類、門面類、代理類一般被設計爲單例類。像Java的Struts、spring框架,.Net的Spring.Net框架,以及PHP的Zend框架都大量使用單例模式。

4.單例模式的5種不一樣寫法及其總結

  單例模式的實現經常使用的有5種,分別是:

  (1).餓漢式;

  (2).懶漢式(、加同步鎖的懶漢式、加雙重校驗鎖的懶漢式、防止指令重排優化的懶漢式);

  (3).登記式單例模式;

  (4)靜態內部類單例模式;

  (5).枚舉類型的單例模式。

  接下來,我就以代碼爲主來對各類實現方式介紹一下。

項目工程結構:如圖中的紅框1中所示。

 

(1).餓漢式

代碼清單【1】

 1 package com.lxf.singleton;
 2 
 3 /**
 4 
 5  * 單例類--餓漢模式   線程安全
 6  * @author Administrator
 7  * 
 8  */
 9 public class Singleton 
10 {
11     private static final Singleton INSTANCE = new Singleton();
12     
13     private static boolean flag = true;
14     private Singleton()
15     {
16     }
17     
18     public static Singleton newInstance()
19     {
20         return INSTANCE;
21     }
22 
23 }

  從代碼中,咱們能夠看到,該類的構造函數被定義爲private,這樣就保證了其餘類不能實例化此類,而後該單例類提供了一個靜態實例並返回給調用者(向外界提供了調用該類方法的實例)。餓漢模式在類加載的時候就對該實例進行建立,實例在整個程序週期都存在。

  優勢:只在類加載的時候建立一次,不會存在多個線程建立多個實例的狀況,避免了多線程同步的問題,是線程安全的。
  缺點:在整個程序週期中,即便這個單例沒有被用到也會被加載,並且在類加載以後就被建立,內存就被浪費了。
  使用場景:適合單例佔用內存比較小,在初始化就被用到的狀況。可是,若是單例佔用的內存比較大,或者單例只是在某個場景下才會被使用到,使用該模式就不合適了,這時候就要考慮使用「懶漢模式」進行延遲加載。

(2).懶漢式(、加同步鎖的懶漢式、加雙重校驗鎖的懶漢式、防止指令重排優化的懶漢式)

2.1--懶漢式(、加同步鎖的懶漢式

代碼清單【2.1】

 1 package com.lxf.singleton;
 2 
 3 /**
 4  * 懶漢式單例模式   線程不安全
 5  * @author Administrator
 6  *
 7  */
 8 public class Singleton2 
 9 {
10     private static Singleton2 instance = null;
11     
12     private Singleton2(){}
13 
14     /*
15      * 1.未加同步鎖
16      */
17     /*
18     public static Singleton2 getInstance()
19     {
20         if(instance == null)
21         {
22             instance = new Singleton2();
23         }
24         return instance;
25     }
26     */
27     
28     /*
29      * 2.加同步鎖     線程安全
30      * 上面的懶漢模式並無考慮多線程的安全問題,在多性格線程可能併發調用它的getInsatance()方法,
31      * 致使建立多個實例,所以須要加鎖來解決線程同步問題。
32      */
33     public static synchronized Singleton2 getInstance()
34     {
35         if(instance == null)
36         {
37             instance = new Singleton2();
38         }
39         return instance;
40     }
41
46 }

  懶漢式單例模式是在須要的時候纔去建立,若是調用該接口獲取實例的時候,發現該實例不存在,就會被建立;若是發現該實例已經存在,就會返回以前已經建立出來的實例。

  可是懶漢模式的單例設計,是線程不安全的,沒有考慮線程安全問題。若是你的程序是多線程的,而這些線程可能會同時運行這段代碼。若是每次運行結果和單線程的運行結果同樣的,並且其餘的變量的值也和預期同樣的,就是線程安全的。顯然,懶漢式單例模式並非線程安全的,在多線程併發環境下,可能會建立出來多個實例。

  使用場景:適合在項目中使用單例類數量較少,並且佔用資源比較多的項目,能夠考慮使用懶漢式單例模式。

2.2--加雙重校驗鎖的懶漢式、防止指令重排優化的懶漢式

 

代碼清單【2.2】

 

 1 package com.lxf.singleton;
 2 
 3 /**
 4  * 雙重校驗鎖     線程安全
 5  * @author Administrator
 6  * 
 7  */
 8 public class Singleton3
 9 {
10     private static Singleton3 instance = null;
11     //禁止指令重排優化
12     //private static volatile Singleton3 instance = null;
13     private Singleton3(){}
14     
15     public static Singleton3 getInstance()
16     {
17         if(null == instance)
18         {
19             synchronized (Singleton3.class)
20             {
21                 if(null == instance)
22                 {
23                     //雙重校驗
24                     instance = new Singleton3();
25                 }
26                 
27             }
28         }
29         return instance;
30     }
31 
32 }

 

 

 

   在加鎖的懶漢模式中,看似解決了線程的併發安全問題,有實現了延遲加載,然而它存在着性能問題。synchronized修飾的同步方法比通常方法要慢不少,若是屢次調用getInstance(),累積的性能損耗就比較大了。所以,咱們這裏就有了雙重校驗鎖。在上面的雙重校驗鎖代碼中,因爲單例對象只須要建立一次,若是後面再次調用getInstance()只須要直接返回單例對象。

     所以,大部分狀況下,調用getInstance()都不會執行到同步代碼塊中的代碼,從而提升了性能

     不過,在這裏要提到Java中的指令重排優化。指令重排優化:在不改變原語義的狀況下,經過調整指令的執行順序讓程序運行的更快。

     因爲指令重拍優化的存在,致使初始化Singleton3和將對象地址付給instance字段的順序是不肯定的。好比:在某個線程建立單例對象時,在構造方法被調用以前,就爲該對象分配了內存空間並將對象的字段設置爲默認值。此時就能夠將分配的內存地址複製給instance字段了,而後該對象可能尚未初始化。若緊接着另一個線程來調用getInstance,獲得的是狀態不肯定的對象,程序就會出錯。

  以上就是雙重校驗鎖會失效的緣由。不過在JDK1.5及其之後的版本中,增長了volatile關鍵字。volatile的關鍵字的一個語義就是禁止指令重排優化,這樣就保證了instance變量被賦值的時候已是初始化的,避免了上面提到的狀態不肯定的問題。

3.登記式單例模式

代碼清單【3】

 1 package com.lxf.singleton;
 2 
 3 import java.util.HashMap;
 4 import java.util.Map;
 5 
 6 /**
 7  * 登記式單例模式   線程安全
 8  * @author Administrator
 9  *就是將該類名進行登記,每次調用前查詢,若是存在,則直接使用;不存在,則進行登記。
10  *這裏使用Map<String,Class>
11  */
12 public class Singleton4 
13 {
14     private static Map<String, Singleton4> map = new HashMap<String, Singleton4>();
15     
16     /*
17      * 靜態語句塊,保證其中的內容在類加載的時候運行一次,而且只運行一次。
18      */
19     static 
20     {
21         Singleton4 singleton4 = new Singleton4();
22         map.put(singleton4.getClass().getName(), singleton4);
23     }
24     
25     //保護的默認構造子
26     protected Singleton4 (){}
27     //靜態工廠方法,返回此類惟一的實例
28     public static Singleton4 getInstance(String name)
29     {
30         if(null == name)
31         {
32             name = Singleton4.class.getName();
33             System.out.println("name == null  --- > name == " + name);
34         }
35         if(null == map.get(name))
36         {
37             try {
38                 map.put(name, (Singleton4) Class.forName(name).newInstance());
39             } catch (InstantiationException e) {
40                 e.printStackTrace();
41             } catch (IllegalAccessException e) {
42                 e.printStackTrace();
43             } catch (ClassNotFoundException e) {
44                 e.printStackTrace();
45             }
46         }
47         return map.get(name);
48     }
49     
50 }

 

 4.靜態內部類單例模式;

代碼清單【4】

 1 package com.lxf.singleton;
 2 /**
 3  * 靜態內部類單例模式  線程安全
 4  * @author Administrator
 5  */
 6 public class Singleton5 
 7 {
 8     /*
 9      * 內部類,用於實現延遲機制
10      * @author Administrator
11      */
12     private static class SingletonHolder
13     {
14         private static Singleton5 instance = new Singleton5();
15     }
16     //私有的構造方法,保證外部的類不能經過構造器來實例化
17     private Singleton5(){}
18     
19     /*
20      *獲取單例對象的實例
21      */
22     public static Singleton5 getInstacne()
23     {
24         return SingletonHolder.instance;
25     }
26 
27 }

  這種方式一樣利用了類加載機制來保證只建立一個insatcne實例。所以不存在多線程併發的問題。它是在內部類裏面去建立對象實例,這樣的話,只要應用中不使用內部類,JVM就不會去加載這個單例類
也就不會加載單例對象,從而實現了延遲加載。

 

5.枚舉類型的單例模式。

代碼清單【5】

package com.lxf.singleton;

/**
 * 咱們要建立的單例類資源,好比:數據庫鏈接,網絡鏈接,線程池之類的。   
 * @author Administrator
 *
 */
class Resource
{
    public void doMethod()
    {
        System.out.println("枚舉類型的單例類資源");
    }
}

/**
 * 枚舉類型的單例模式   線程安全
 * 
 * 獲取資源的方式,Singleton6.INSTANCE.getInstance();便可得到所要的實例。
 * @author Administrator
 *
 */


public enum Singleton6
{
    INSTANCE;
    
    private Resource instance;
    
    Singleton6()
    {
        instance = new Resource();
    }
    
    public Resource getInstance()
    {
        return instance;
    }
    
    
}

  上面代碼中,首先,在枚舉中咱們明確了構造方法限制爲私有,在咱們訪問枚舉實例時會執行構造方法,同時每一個枚舉實例都是static final類型的,也就代表只能被實例化一次。在調用構造方法時,咱們的單例被實例化。也就是說,由於enum中的實例被保證只會被實例化一次,因此咱們的INSTANCE也就會被實例化一次。
  在以前介紹的實現單例的方式中都有共同的缺點:
(1).須要額外的工做來實現序列化,不然每次反序列化一個序列化的對象時都會建立一個新的實例;
(2).可使用反射強行調用私有構造器(若是要避免這個狀況,能夠修改構造器,讓它在建立第二我的實例的時候拋異常。)這個會在第5點中進行介紹
  而使用枚舉出了線程安全和防止反射調用構造器以外,還提供了自動序列化機制,防止反序列化的時候建立新的對象。

6.單例模式測試類   SingletonMain.java

 代碼清單【6】

  1 package com.lxf.singleton;
  2 
  3 import org.junit.Test;
  4 
  5 public class SingletonMain 
  6 {
  7     /**
  8      *1. 餓漢模式單例測試
  9      */
 10     @Test
 11     public void testSingletonTest()
 12     {
 13         System.out.println("-------餓漢模式單例測試--------------");
 14         Singleton singleton = Singleton.newInstance();
 15         //singleton.about();
 16         Singleton singleton2 = Singleton.newInstance();
 17         //singleton2.about();
 18         
 19         if(singleton == singleton2)
 20         {
 21             System.out.println("1.singleton and singleton2 are same Object");
 22         }
 23         else
 24         {
 25             System.out.println("1.singleton and singleton2 aren't same Object");
 26         }
 27         System.out.println("---------------------------------------------");
 28     }
 29     
 30     /**
 31      *2. 懶漢式單例模式測試
 32      */
 33     @Test
 34     public void testSingleton2Test()
 35     {
 36         System.out.println("-------懶漢式單例模式測試--------------");
 37         Singleton2 singleton = Singleton2.getInstance();
 38         Singleton2 singleton2 = Singleton2.getInstance();
 39         
 40         if(singleton == singleton2)
 41         {
 42             System.out.println("2.singleton and singleton2 are same Object");
 43         }
 44         else
 45         {
 46             System.out.println("2.singleton and singleton2 aren't same Object");
 47         }
 48         System.out.println("---------------------------------------------");
 49     }
 50     
 51     /**
 52      * 3.雙重校驗鎖單例模式測試
 53      */
 54     @Test
 55     public void testSingleton3()
 56     {
 57         System.out.println("-------雙重校驗鎖單例模式測試--------------");
 58         Singleton3 singleton = Singleton3.getInstance();
 59         Singleton3 singleton2 = Singleton3.getInstance();
 60         
 61         if(singleton == singleton2)
 62         {
 63             System.out.println("3.singleton and singleton2 are same Object");
 64         }
 65         else
 66         {
 67             System.out.println("3.singleton and singleton2 aren't same Object");
 68         }
 69         System.out.println("---------------------------------------------");
 70     }
 71     
 72     /**
 73      * 4.登記式單例模式測試
 74      */
 75     @Test
 76     public void testSingleton4()
 77     {
 78         System.out.println("-------雙重校驗鎖單例模式測試--------------");
 79         Singleton4 singleton = Singleton4.getInstance(Singleton4.class.getName());
 80         Singleton4 singleton2 = Singleton4.getInstance(Singleton4.class.getName());
 81         if(singleton == singleton2)
 82         {
 83             System.out.println("4.singleton and singleton2 are same Object");
 84         }
 85         else
 86         {
 87             System.out.println("4.singleton and singleton2 aren't same Object");
 88         }
 89         System.out.println("---------------------------------------------");
 90     }
 91     
 92     /**
 93      *5. 靜態內部類單例模式測試
 94      */
 95     @Test
 96     public void testSingleton5()
 97     {
 98         System.out.println("-------靜態內部類單例模式測試--------------");
 99         Singleton5 singleton = Singleton5.getInstacne();
100         Singleton5 singleton2 = Singleton5.getInstacne();
101         if(singleton == singleton2)
102         {
103             System.out.println("5.singleton and singleton2 are same Object");
104         }
105         else
106         {
107             System.out.println("5.singleton and singleton2 aren't same Object");
108         }
109         System.out.println("---------------------------------------------");
110     }
111     
112     /**
113      *6. 靜態內部類單例模式測試
114      */
115     @Test
116     public void testSingleton6()
117     {
118         System.out.println("-------枚舉類型的單例類資源測試--------------");
119         Resource singleton = Singleton6.INSTANCE.getInstance();
120         Resource singleton2 = Singleton6.INSTANCE.getInstance();
121         if(singleton == singleton2)
122         {
123             System.out.println("6.singleton and singleton2 are same Object");
124         }
125         else
126         {
127             System.out.println("6.singleton and singleton2 aren't same Object");
128         }
129         System.out.println("---------------------------------------------");
130     }
131     
132 
133 }

 

運行結果:

 

5.拓展--如何防止Java反射機制對單例類的攻擊?

  上面介紹的除了最後一種枚舉類型單例模式外,其他的寫法都是基於一個條件:確保不會被反射機制調用私有的構造器。
那麼如何防止Java反射機制對單例類的攻擊呢?請參考下一篇隨筆:《如何防止反射機制對單例類的攻擊?》

 

 6.後期補充

相關文章
相關標籤/搜索