程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 設計模式之單例模式,設計模式之模式

設計模式之單例模式,設計模式之模式

編輯:JAVA綜合教程

設計模式之單例模式,設計模式之模式


單例模式是軟件開發中非常普遍的一種模式。它的主要作用是確保系統中,始終只存在一個類的實例對象。

這樣做的好處有兩點:

1、對於需要頻繁使用的對象,在每次使用時,如果都需要重新創建,並且這些對象的內容都是一樣的。則不但提高了jvm的性能開銷(堆中開辟新地址,同時降低GC效率等),同時還會降低代碼的運行效率。倘若始終在堆中只存在唯一的一個實例對象。任何方法在使用時,均直接訪問這個實例對象,則大大提高了系統的運行效率。

2、可以更好的維護對象,倘若系統中存在多個相同的實例對象,而一旦這些實例對象的屬性發生了改變,則需要通知系統中所有的實例對象均發生相同的改變,才能保證數據的有效性和唯一性。但是當系統的復雜度到達一定的量級後,維護這種場景的開銷會越來越大。比如:如何通知到所有的類實例?或者出現多線程場景後,如何保證所有的實例對象的屬性狀態保持同步修改?單例模式可以很好的解決這個問題,因為整個系統中,只存在一個該類的實例對象。

單例模式實現的核心就是,通過創建方法,始終返回的都是一個唯一的實例。

下面依次介紹開發中常用的幾種實現方式,以及他們的優缺點:

最簡單的形式:

 1 public class Singleton
 2 {
 3     private Singleton()
 4     {
 5         //do sth
 6 
 7     }
 8     
 9     private static Singleton instance=new Singleton();
10 
11     public static Singleton getInstance()
12     {
13         return instance;
14     }
15 }


這樣實現單例模式的好處是,實現的邏輯簡單,易於閱讀和使用。缺點是由於instance使用的是類靜態字段並且直接初始化,所以在jvm加載該類時,就會直接創建該實例。而我們或許始終都不會使用該實例。倘若示例中的構造函數do sth部分是非常耗時的部分,則會導致加載類的初期,系統的響應速度持續走高,並且在jvm堆中始終都會存在這個對象實例,形成內存的浪費。

ps 有些人可能會很難理解,既然jvm加載該類時,就代表我們會使用該對象了,為什麼還會存在該實例不會被使用的場景?這裡舉個例子,比如需要用到這個類的某個靜態字段,或者靜態方法或者這個類被反射到,jvm都會加載該類。

為了解決這個問題,開發者們後來又想到了一種延時加載的方法:

 1 public class Singleton
 2 {
 3     private Singleton()
 4     {
 5         //do sth
 6     }
 7 
 8     private static Singleton instance = null;
 9 
10     public static synchronized Singleton getInstance()
11     {
12         if(instance == null)
13         {
14             instance=new Singleton();
15         }
16         return instance;
17     }
18 }

之所以給這個方法加入一個同步保護,是由於可能存在多線程的場景,線程A首先進入獲取實例的方法,判斷instance為null,則開始運行構造函數,而線程B同時進入該方法,由於構造方法尚未運行結束,因此instance仍然為null,所以線程B仍然會調用構造函數。從而破壞單例的唯一性。

但是單例,勢必會造成線程等待,我們讓單例類的構造函數只運行一次,為的就是快,而現在反而又為了線程安全,使速度降下來。有些人或許會覺得一個小小的同步,影響性能並不大,可是如果出現高並發時,最後一個線程等待的時間,是之前線程等待時間的累加,《java程序性能優化》書中曾經做過嘗試,在五個線程同時調用以上代碼時,耗費時間是390ms,而非延時加載的方法(第一種方法)耗時為0ms(也就是未到達1個ms),兩者相差甚多。

不延時,可能會讓系統無用開銷過多,而延時又為了保證線程安全,造成額外的開銷,究竟應該使用哪種呢?

我個人建議,如果是服務端的話(客戶端則更多的需要根據使用場景來斟酌),建議使用第一種。原因如下:

1)方法簡潔,不容易出錯。(這個我認為非常重要,很多人可能覺得無所謂)

2)硬件現在越來越廉價,用空間換時間大部分情況下是非常劃算的。

3)大部分客戶端更關心的是服務器在運行期的響應時間,而非服務器在啟動時的快慢。(這裡的表述不太嚴謹)

盡管如此,我們還是希望又可以做到延時加載,又能不讓線程存在等待。於是有人想到了以下的方式:

 1 public class Singleton
 2 {
 3     private Singleton()
 4     {
 5         //do sth
 6     }
 7 
 8     private static Singleton instance = null;
 9 
10     public static Singleton getInstance()
11     {
12         if(instance==null)
13         {
14             synchronized(Singleton.class)
15             {
16                 if(instance==null)
17                 {
18                     instance=new Singleton();
19                 }
20             }
21         }
22         return instance;
23     }
24 }

這樣做的好處是,將線程等待的區間段縮減至最低,只在類初期初始化時,增加線程安全的保護。倘若已經創建成功,則再次獲取實例的線程是不需要再次等待的。

個人不建議這種寫法,因為看著別扭,不方便閱讀,雙重鎖盡管使用廣泛,但是畢竟第一次閱讀時,還是需要仔細分析下,畢竟java中還有很多其他實現單例的優雅的方式。

ps 該種方法並不適用於在JDK1.5之前,這並不是由於語法的錯誤,而是由於java的內存模型自身的問題:簡而言之就是,由於jvm指令順序的優化,可能會導致先給instance賦予了一段堆內存,然後才在該堆內存上初始化該對象。在instance變量賦值成功後,退出同步代碼塊。新線程進入判斷條件,發現instance仍然未初始化,所以再次開始初始化該變量。導致instance被反復初始。在jdk1.5以後推出了volatile關鍵字,我們可以用該關鍵字修飾instance變量,從而防止jvm優化該段指令。

那麼還有什麼辦法來解決這個方法呢?聰明的人想到了使用內部類來保存instance的持有。

 

 1 public class Singleton
 2 {
 3     private Singleton() 
 4     {
 5         // do sth
 6     }
 7 
 8     private static class SingletonInner
 9     {
10         private static Singleton instance = new Singleton();
11     }
12 
13     public static Singleton getInstance()
14     {
15         return SingletonInner.instance;
16     }
17 }

前文所述的例子,其實無外乎存在兩個問題,第一最好使用延時加載,最好延時加載的時機是我真正要用到實例的時候,而非加載單例類的時候。第二,開始使用前,就已經加載好單例了,別讓我出現等待。

而靜態內部類可以很好的解決這個問題:1加載該類的時候(調用靜態字段,靜態方法時),並不會調用構造函數創建實例。2真正需要實例時,實例是保存在在靜態內部類中的字段的,靜態內部類此時才會被加載,而單例類此時就會創建實例<clinit>()方法,所以多線程進入時,字段已經被初始化完畢了。這種形式的單例也是我非常喜歡的一種單例形式,不但閱讀方便,同時還很好的彌補了其他單例的一些弊端。

最後再介紹一種利用關鍵字很好的解決了單例問題的方式:

什麼關鍵字生來就可以保證一個實例而生的呢?這就是枚舉。

先看代碼

 1 public enum Singleton
 2 {
 3     instance();
 4     Singleton() 
 5     {
 6         // do sth
 7     }
 8 
 9     public final void A()
10     {
11 
12     }
13 }


了解枚舉的人都知道每一個枚舉項都是該類的一個實例,而該類也不可以再創造出其他更多的實例。同時通過反射和正反序列化的形式,其實是可以突破前文中示例的單例限制的,即創造出多個實例(雖然如此,我也沒怎麼見過需要各種防范這些問題的)。而使用枚舉,可以通過java自身的機制,很好的解決這些問題。這也是《Effective java》的作者非常建議的形式。不過盡管這本書非常暢銷,而且評價很高,但是卻很少見到使用這種寫法的地方。

說了這麼多,我們也應該再來談談單例模式的缺點:

1、單例模式不容易拓展,類的構造函數被私有化,子類根本無法執行父類的構造方法

2、開發過程中,為了盡可能的保證,單例一旦構造好,就可以方便直接使用的目的,往往在單例中加入大量的方法,從而使單例類的職責很模糊,很多功能無法界定是否應該由該類來負責,違反了面相對象的基本原則。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved