1. 程式人生 > >java 單例模式

java 單例模式

多線程安全 except detail 追加 earch 繼承 好處 config 什麽是

單例模式(Singleton)也叫單態模式,是設計模式中最為簡單的一種模式,甚至有些模式大師都不稱其為模式,稱其為一種實現技巧,因為設計模式講究對象之間的關系的抽象,而單例模式只有自己一個對象,也因此有些設計大師並把把其稱為設計模式之一。

這裏又不具體講如何實現單例模式和介紹其原理(因為這方便的已經有太多的好文章介紹了),如果對單例模式不了解的可以先看下:http://terrylee.cnblogs.com/archive/2005/12/09/293509.html 。當然也可以自己搜索。

好多沒怎麽使用過的人可能會想,單例模式感覺不怎麽用到,實際的應用場景有哪些呢?以下,我將列出一些就在咱們周邊和很有意義的單例應用場景。

1. Windows的Task Manager(任務管理器)就是很典型的單例模式(這個很熟悉吧),想想看,是不是呢,你能打開兩個windows task manager嗎? 不信你自己試試看哦~

2. windows的Recycle Bin(回收站)也是典型的單例應用。在整個系統運行過程中,回收站一直維護著僅有的一個實例。

3. 網站的計數器,一般也是采用單例模式實現,否則難以同步。

4. 應用程序的日誌應用,一般都何用單例模式實現,這一般是由於共享的日誌文件一直處於打開狀態,因為只能有一個實例去操作,否則內容不好追加。

5. Web應用的配置對象的讀取,一般也應用單例模式,這個是由於配置文件是共享的資源。

6. 數據庫連接池的設計一般也是采用單例模式,因為數據庫連接是一種數據庫資源。數據庫軟件系統中使用數據庫連接池,主要是節省打開或者關閉數據庫連接所引起的效率損耗,這種效率上的損耗還是非常昂貴的,因為何用單例模式來維護,就可以大大降低這種損耗。

7. 多線程的線程池的設計一般也是采用單例模式,這是由於線程池要方便對池中的線程進行控制。

8. 操作系統的文件系統,也是大的單例模式實現的具體例子,一個操作系統只能有一個文件系統。

9. HttpApplication 也是單位例的典型應用。熟悉ASP.NET(IIS)的整個請求生命周期的人應該知道HttpApplication也是單例模式,所有的HttpModule都共享一個HttpApplication實例.

總結以上,不難看出:

  單例模式應用的場景一般發現在以下條件下:

  (1)資源共享的情況下,避免由於資源操作時導致的性能或損耗等。如上述中的日誌文件,應用配置。

  (2)控制資源的情況下,方便資源之間的互相通信。如線程池等。

設計模式(一):單例模式(Singleton Pattern)

單例模式(Singleton Pattern)是設計模式中比較常用的一種,下面來總結單例模式的知識,包括:

1、理解什麽是單例模式、單例模式有什麽優點/缺點、單例模式的應用場景;

2、再來看看Java單例模式的6種代碼實現方式、每種實現方式有什麽需要註意的;

3、後面再來了解Java單例模式其他值得關註的地方,如比較靜態方法、以及Java反射、反序列化、垃圾回收的影響等。

1、什麽是單例模式

1-1、模式理解

保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。

UML結構圖:

技術分享

模式角色:

一個類使用了單例模式,稱該類為單例類,如圖中的Singleton。

單例模式三要點:

(1)、單例類只能有一個實例

這是最基本的,真正做到整個系統中唯一並不容易,通常還要考慮反射破壞、序列化/反序列化、對象垃圾回收等問題。

(2)、單例類必須自己創建自己的唯一實例

通常給實例構造函數protected或private權限。

(3)、單例類必須給所有其他對象提供這一實例

通常定義靜態方法getInstance()返回。

1-2、特點

優點:

(1)、提供了對唯一實例的受控訪問,避免對資源的多重占用。

(2)、在內存裏只有一個實例,減少了內存的開銷,尤其是頻繁的創建和銷毀實例。

(3)縮小名空間,避免全局變量汙染空間,但比類操作更靈活。

缺點:

(1)、由於單例模式中沒有抽象層,因此單例類的擴展有很大的困難。

(2)、 單例類的職責過重,在一定程度上違背了"單一職責原則"。

因為單例類既充當了工廠角色,提供了工廠方法,同時又充當了產品角色,包含一些業務方法,將產品的創建和產品的本身的功能融合到一起。

所以也不應過多使用單例模式。

1-3、應用

單例模式是一種對象創建型模式,用來編寫一個類,在整個應用系統中只能有該類的一個實例對象。

常見應用場景:

線程池、緩存、日誌、配置文件、打印機/顯卡等硬件設備的驅動程序對象等等。

JDK中的一些應用:

java.lang.Runtime#getRuntime()

java.text.NumberFormat#getInstance()

java.awt.GraphicsEnvironment#getLocalGraphicsEnvironment()

面向對象的設計模式可以說是一種“套路”,是不分編程語言的,而後面主要以Java言語為主。

2、單例模式實現示例

Java單例模式的實現有多種方式,使用Java實現如下所示。

2-1、餓漢式(簡單可用)

Lazy 初始化:否;

多線程安全:是;

描述:

這種方式比較常用,它基於JVM的類加載器機制避免了多線程的同步問題,對象在類裝載時就實例化,所以稱為餓漢式。

優點:沒有加鎖,執行效率會提高。

缺點:沒有Lazy初始化,可能有時候不需要使用,浪費內存。

代碼實例:

[java] view plain copy
  1. public class Singleton {
  2. private static Singleton instance = new Singleton();
  3. private Singleton (){}
  4. public static Singleton getInstance() {
  5. return instance;
  6. }
  7. }

2-2、懶漢式(線程不安全,不可用)

Lazy 初始化:是;

多線程安全:否;

描述:

能夠在getInstance()時再創建對象,所以稱為懶漢式。這種實現最大的問題就是不支持多線程。因為沒有加鎖同步。

代碼實例:

[java] view plain copy
  1. public class Singleton {
  2. private static Singleton instance;
  3. private Singleton (){}
  4. public static Singleton getInstance() {
  5. if (instance == null) {
  6. instance = new Singleton();
  7. }
  8. return instance;
  9. }
  10. }

2-3、同步方法的懶漢式(同步方法效率低,不推薦)

Lazy 初始化:是

多線程安全:是

描述:

除第一次使用,後面getInstance()不需要同步;每次同步,效率很低。

代碼實例:

[java] view plain copy
  1. public class Singleton {
  2. private static Singleton instance;
  3. private Singleton (){}
  4. public static synchronized Singleton getInstance() {
  5. if (instance == null) {
  6. instance = new Singleton();
  7. }
  8. return instance;
  9. }
  10. }

2-4、雙重校驗鎖(可用)

Lazy 初始化:是;

多線程安全:是;

描述:

這種方式采用雙鎖機制,安全且在多線程情況下能保持高性能。

實例變量需要加volatile 關鍵字保證易變可見性,JDK1.5起才可用。

代碼實例:

[java] view plain copy
  1. public class Singleton {
  2. private volatile static Singleton singleton;
  3. private Singleton (){}
  4. public static Singleton getSingleton() {
  5. if (singleton == null) {
  6. synchronized (Singleton.class) {
  7. if (singleton == null) {
  8. singleton = new Singleton();
  9. }
  10. }
  11. }
  12. return singleton;
  13. }
  14. }

2-5、靜態內部類(推薦)

Lazy 初始化:是;

多線程安全:是;

描述:

同樣利用了JVM類加載機制來保證初始化實例對象時只有一個線程,靜態內部類SingletonHolder 類只有第一次調用 getInstance 方法時,才會裝載從而實例化對象。

代碼實例:

[java] view plain copy
  1. public class Singleton {
  2. private static class SingletonHolder {
  3. private static final Singleton INSTANCE = new Singleton();
  4. }
  5. private Singleton (){}
  6. public static final Singleton getInstance() {
  7. return SingletonHolder.INSTANCE;
  8. }
  9. }

2-6、枚舉(《Effective Java》推薦,不常見)

Lazy 初始化:否;

多線程安全:是;

描述:

從Java1.5開始支持enum特性;無償提供序列化機制,絕對防止多次實例化,即使在面對復雜的序列化或者反射攻擊的時候。

不過,用這種方式寫不免讓人感覺生疏,這種實現方式還沒有被廣泛采用,但這是實現單例模式的最佳方法。

代碼實例:

[java] view plain copy
  1. public enum Singleton {
  2. //定義一個枚舉的元素,就代表Singleton實例
  3. INSTANCE;
  4. /*
  5. **假如還定義有下面的方法,調用:Singleton.INSTANCE.doSomethingMethod();
  6. */
  7. public void doSomethingMethod() {
  8. }
  9. }

2-7、小結

以上6種單例實現方式,不是線程安全的不能用,至於是否需要延時加載,看情況而定。

一般情況下,使用最基本、最簡單的第一種餓漢式就行了(JDK中有不少使用該種方式),需要延時加載的使用靜態內部類方式,需要高安全性的可以使用第6種枚舉方式

3、其他關註點

3-1、單例模式VS靜態類(靜態屬性/方法)

把類中所有屬性/方法定義成靜態也可以實現"單例"。

那為什麽需要用"NEW"單例模式,在而不把類中所有屬性/方法定義成靜態的?

靜態類不用實例化就可以使用,雖然使用比較方便,但失去了面向對象的一些優點,適用於一些過程簡單且固定、不需要擴展變化、不需要維護任何狀態的類方法,如java.lang.Math,裏面每種計算方法基本都是固定不變的。

單例模式保證一個類對象實例的唯一性,有面向對象的特性,雖然擴展不容易,但還是可以被繼承(protected權限的構造方法)、重寫方法等。

3-2、Java反射攻擊破壞單例模式

上面6種Java單例模式實現方式除枚舉方式外,其他的給實例構造函數protected或private權限,依然可以通過相關反射方法,改變其權限,創建多個實例,如下:

[java] view plain copy
  1. public class Test {
  2. public static void main(String args[]) {
  3. Singleton singleton = Singleton.getInstance();
  4. try {
  5. Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
  6. constructor.setAccessible(true);
  7. Singleton singletonnew = constructor.newInstance();
  8. System.out.println(singleton == singletonnew);
  9. } catch (Exception e) {
  10. }
  11. }
  12. }

輸出結果:false

可以給構造函數加上判斷,限制創建多個實例,如下:

[java] view plain copy
  1. private Singleton() {
  2. if (null != Singleton.singleton) {
  3. throw new RuntimeException();
  4. }
  5. }

3-3、反序列化攻擊破壞單例模式

很多語言、框架都支持對象的序列化,對象序列化後再進行存儲或傳輸,以獲得更好的效率,之後再反序列化得到同樣的對象信息。

同樣,前面6種Java單例模式實現方式除枚舉方式外,其他方式用一樣的序列化數據,可以多次反序列出多個不同的實例對象。

對於Java語言提供的序列化/反序列化機制,需要單例類實現java.io.Serializable接口;而在在反序列化時會調用實例的readResolve()方法,只要加入該方法,並在方法中指定返回單例對象,就不會再新建一個對象,如下:

[java] view plain copy
  1. private Object readResolve() {
  2. return Singleton.singleton;
  3. }

另外,最好還要確保該類的所有實例域都為基本類型,或者是transient的。否則,還是可能受到攻擊破壞。

更多信息,可以參考:

《Effective Java》第二版 第77條:對於實例控制,枚舉類型優先於readResolve

3-4、單例模式中的單例對象會不會被垃圾回收?

對於JDK1.2後的JVM HotSpot來說,判斷對象可以回收需要經過可達性分析,由於單例對象被其類中的靜態變量引用,所以JVM認為對象是可達的,不會被回收。

另外,對於JVM方法區回收,由堆中存在單例對象,所以單例類也不會被卸載,其靜態變量引用也不會失效。

3-5、多JVM/ClassLoader的系統使用單例類

不同ClassLoader加載同一個類,對類本身的對象(Singleton.class)來說是不一樣的,所以可以創建出不同的單例對象,對不同JVM的情況更是如此,這些在JavaEE開發中還是比較常見。

所以,在多JVM/ClassLoader的系統使用單例類,需要註意單例對象的狀態,最好使用無狀態的單例類。

3-6、Spring(IOC框架)實現的單例

Spring的一個核心功能控制反轉(Inversion of Contro,IOC),或稱依賴註入(dependency injection ,DI):
高層模塊通過接口編程,然後通過配置Spring的XML文件或註解來註入具體的實現類(Bean)。
這樣的好處的很容易擴展,想要更換其他實現類時,只需要修改配置就可以了。
其功能是通過IOC容器來實現,其默認生成的Bean是單例的:
在整個應用中(一般只用一個IOC容器),只創建Bean的一個實例,多次註入同一具體類時都是註入同一個實例。
IOC容器來實現過程簡述如下:
當需要註入Bean時,IOC容器首先解析配置找到具體類,然後判斷其作用域(@Scope註解);
[email protected](ConfigurableBeanFactory.SCOPE_SINGLETON),則查找容器中之前有沒有為其創建了Bean實例;

如果有則直接註入該Bean實例,如果沒有生成一個放到容器中保存(ConcurrentHashMap -- map.put(bean_id, bean)),再註入。

註:其中解析配置查找具體類、生成Bean實例和註入過程都是通過Java反射機制實現的。

從上面可以了解到,Spring實現的單例和我們所說的單例設計模式不是一個概念:
前者是IOC容器通過Java反射機制實現,後者只是一種編程方法(套路)。
但總的來說,它們都可以實現“單例”。

4、總結

單例模式:

(1)、單例模式可以在一些應用場景帶來很好的效果,但不能濫用,因為單例模式並不是一種很好的模式。

(2)、單例模式有多種實現方式,沒有特殊要求的,用最基本、最簡單的餓漢式,需要延時加載的使用靜態內部類方式,需要高安全性的可以使用枚舉方式;

(3)、對其他關註點應有所了解,有時間可以深入探究,擴展知識面。

到這裏,我們對單例模式有了一個大體的了解,後面我們將了解其他的設計模式......

java 單例模式