利用元旦小假期,參考了幾篇單例模式介紹的文章,而後本身下午對java設計模式中的單例模式作了一下簡單的總結,主要是代碼介紹。java
單例模式,在實際項目開發中運用普遍,好比數據庫鏈接池,實際上,配置信息類、管理類、控制類、門面類、代理類一般被設計爲單例類。像Java的Struts、spring框架,.Net的Spring.Net框架,以及PHP的Zend框架都大量使用單例模式。spring
那麼,接下來,我將如下面5點來對單例模式做一下介紹:數據庫
1.單例模式的定義設計模式
2.單例模式的特色安全
3.爲何要使用單例模式?網絡
4.單例模式的5種不一樣寫法及其總結多線程
5.拓展--如何防止Java反射機制對單例類的攻擊?併發
單例模式(Singleton)是一種建立型模式,指某個類採用Singleton模式,則在這個類被建立後,只可能產生一個實例供外部訪問,而且提供一個全局的訪問點。框架
核心知識點:
(1)將採用單例模式的類的構造方法私有化(採用private修飾);
(2)在其內部產生該類的實例化對象,並將其封裝成private static類型;
(3)定義一個靜態方法返回該類的實例。函數
- 單例類只能有一個實例;
- 單例類必須本身建立本身的惟一實例;
- 單例類必須給全部其餘對象提供這一實例。
根據單例模式的定義和特色,咱們會對單例模式有了初步認識,那麼由特色出發,單例模式在項目中的做用就顯而易見了。
(1)控制資源的使用,經過線程同步來控制資源的併發訪問;
(2)控制實例產生的數量,到達節約資源的目的;
(3)做爲通訊媒介使用,也就是數據共享,他能夠在不創建直接關聯的條件下,讓多個不相關的兩個線程或者進程實現通訊。
好比:
數據庫鏈接池的設計通常採用單例模式,數據庫鏈接是一種數據資源。項目中使用數據庫鏈接池,主要是節省打開或者關閉數據庫所引發的效率損耗。固然,使用數據庫鏈接池能夠屏蔽不一樣數據數據庫之間的差別,實現系統對數據庫的低度耦合,也能夠被多個系統同時使用,具備高科複用性,還能方便對數據庫鏈接的管理等。
實際上,配置信息類、管理類、控制類、門面類、代理類一般被設計爲單例類。像Java的Struts、spring框架,.Net的Spring.Net框架,以及PHP的Zend框架都大量使用單例模式。
單例模式的實現經常使用的有5種,分別是:
(1).餓漢式;
(2).懶漢式(、加同步鎖的懶漢式、加雙重校驗鎖的懶漢式、防止指令重排優化的懶漢式);
(3).登記式單例模式;
(4)靜態內部類單例模式;
(5).枚舉類型的單例模式。
接下來,我就以代碼爲主來對各類實現方式介紹一下。
項目工程結構:如圖中的紅框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.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】
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 }
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】
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】
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 }
運行結果:
上面介紹的除了最後一種枚舉類型單例模式外,其他的寫法都是基於一個條件:確保不會被反射機制調用私有的構造器。
那麼如何防止Java反射機制對單例類的攻擊呢?請參考下一篇隨筆:《如何防止反射機制對單例類的攻擊?》