1. 程式人生 > >一篇很不錯的單例模式詳解

一篇很不錯的單例模式詳解

1、什麼是設計模式?

 

    首先我們來看第一個問題什麼是設計模式?在百度百科中它的定義是這樣的: 設計模式(Design pattern)是一套被反覆使用、多數人知曉的、經過分類編目的、程式碼設計經驗的總結。(百度百科)

    其實設計模式是人們實踐的產物,在初期的開發過程中好多人發現再進行重複的程式碼書寫,那些開發大牛們就不斷總結、抽取最終得到了大家的認可於是就產生了設計模式,其實設計模式的種類可以分為23種左右,今天主要和大家一起學習一下單例設計模式,因為這種設計模式是使用的最多的設計模式。在以後的文章中會給大家帶來其他模式的討論。

 

2、為什麼會有單例設計模式?

  我們都知道單例模式是在開發中用的最多的一種設計模式,那麼究竟為什麼會有單例設計模式呢?對於這個問題相信有很多會寫單例的人都會有個這個疑問。在這裡先說一下單例的用途,然後舉一個例子大家就會明白為什麼會有單例了。單例模式主要是為了避免因為建立了多個例項造成資源的浪費,且多個例項由於多次呼叫容易導致結果出現錯誤,而使用單例模式能夠保證整個應用中有且只有一個例項。從其名字中我們就可以看出所謂單例,就是單個例項也就是說它可以解決的問題是:可以保證一個類在記憶體中的物件的唯一性,在一些常用的工具類、執行緒池、快取,資料庫,賬戶登入系統、配置檔案等程式中可能只允許我們建立一個物件,一方面如果建立多個物件可能引起程式的錯誤,另一方面建立多個物件也造成資源的浪費。在這種基礎之上單例設計模式就產生了因為使用單例能夠保證整個應用中有且只有一個例項,看到這大家可能有些疑惑,沒關係,我們來舉一個例子,相信看完後你就會非常明白,為什麼會有單例。

假如有一個有這麼一個需求,有一個類A和一個類B它們共享配置檔案的資訊,在這個配置檔案中有很多資料如下圖

 

 

 

如上圖所示現在類ConfigFile中存在共享的資料Num1,Num2,Num3等。假如在類A中修改ConfigFile中資料,在類A中應該有如下程式碼

  1. ConfigFile configFile=new ConfigFile();
  2. configFile. Num1=2;

這個時候configFile中的Num1=2,但是請注意這裡是new ConfigFile是一個物件,想象一下在進行了上述操作後類B中進行如下操作

  1. ConfigFile configFile=new ConfigFile();
  2. System. out.println("configFile.Num1=" +configFile.Num1);

即直接new ConfigFile();然後列印Num1,大家思考一下這時候打印出的資料為幾?我想你應該知道它列印的結果是這樣的:configFile.Num1=1;也就是說因為每次呼叫都建立了一個ConfigFile物件,所以導致了在類A中的修改並不會真正改變ConfigFile中的值,它所更改的只是在類A中說建立的那個物件的值。假如現在要求在類A中修改資料後,要通知類B,即在類A和類B中操作的資料是同一個資料,類A改變一個數據,類B也會得到這個資料,並在類A修改後的基礎上進行操作,那麼我們應該怎麼做呢?看到這大家可能會說so easy,把ConfigFile中的資料設定為靜態不就Ok了嗎?對,有這種想法很好,這樣做也沒有錯。但是我們都知道靜態資料的生命週期是很長的,假如ConfigFile中有很多資料時,如果將其全部設成靜態的,那將是對記憶體的極大損耗。所以全部設定成靜態雖然可行但並不是一個很好的解決方法。那麼我們應該怎麼做呢?要想解決上面的問題,其實不難,只要能保證物件是唯一的就可以解決上面的問題,那麼問題來了如何保證物件的唯一性呢?這樣就需要用單例設計模式了。

 

3、單例模式的設計思想

在上面我們說到現在解決問題的關鍵就是保證在應用中只有一個物件就行了,那麼怎麼保證只有一個物件呢?

其實只需要三步就可以保證物件的唯一性

(1)不允許其他程式用new物件。

    因為new就是開闢新的空間,在這裡更改資料只是更改的所建立的物件的資料,如果可以new的話,每一次new都產生一個物件,這樣肯定保證不了物件的唯一性。

(2)在該類中建立物件

   因為不允許其他程式new物件,所以這裡的物件需要在本類中new出來

(3)對外提供一個可以讓其他程式獲取該物件的方法

   因為物件是在本類中建立的,所以需要提供一個方法讓其它的類獲取這個物件。

那麼這三步怎麼用程式碼實現呢?將上述三步轉換成程式碼描述是這樣的

(1)私有化該類的建構函式

(2)通過new在本類中建立一個本類物件

(3)定義一個公有的方法,將在該類中所建立的物件返回

 

4、單例模式的寫法

   經過3中的分析我們理解了單例所解決的問題以及它的實現思想,接著來看看它的實現程式碼,單例模式的寫法大的方面可以分為5種五種①懶漢式②餓漢式③雙重校驗鎖④靜態內部類⑤列舉。接下來我們就一起來看看這幾種單例設計模式的程式碼實現,以及它們的優缺點

4.1單例模式的餓漢式[可用]

  1. public class Singleton {
  2.  
  3. private static Singleton instance=new Singleton();
  4. private Singleton(){};
  5. public static Singleton getInstance(){
  6. return instance;
  7. }
  8. }

訪問方式

Singleton instance = Singleton.getInstance();

得到這個例項後就可以訪問這個類中的方法了。

優點:從它的實現中我們可以看到,這種方式的實現比較簡單,在類載入的時候就完成了例項化,避免了執行緒的同步問題。

缺點:由於在類載入的時候就例項化了,所以沒有達到Lazy Loading(懶載入)的效果,也就是說可能我沒有用到這個例項,但是它

也會載入,會造成記憶體的浪費(但是這個浪費可以忽略,所以這種方式也是推薦使用的)。

  4.2單例模式的餓漢式變換寫法[可用]

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

訪問方式:

Singleton instance = Singleton.getInstance();

得到這個例項後就可以訪問這個類中的方法了。

可以看到上面的程式碼是按照在2中分析的那三步來實現的,這中寫法被稱為餓漢式,因為它在類建立的時候就已經例項化了物件。其實4.2和4.1只是寫法有點不同,都是在類初始化時建立物件的,它的優缺點和4.1一樣,可以歸為一種寫法。

4.3單例模式的懶漢式[執行緒不安全,不可用]

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

這種方式是在呼叫getInstance方法的時候才建立物件的,所以它比較懶因此被稱為懶漢式。

在上述兩種寫法中懶漢式其實是存線上程安全問題的,喜歡刨根問題的同學可能會問,存在怎樣的執行緒安全問題?怎樣導致這種問題的?好,我們來說一下什麼情況下這種寫法會有問題。在執行過程中可能存在這麼一種情況:有多個執行緒去呼叫getInstance方法來獲取Singleton的例項,那麼就有可能發生這樣一種情況當第一個執行緒在執行if(instance==null)這個語句時,此時instance是為null的進入語句。在還沒有執行instance=new Singleton()時(此時instance是為null的)第二個執行緒也進入if(instance==null)這個語句,因為之前進入這個語句的執行緒中還沒有執行instance=new Singleton(),所以它會執行instance=new Singleton()來例項化Singleton物件,因為第二個執行緒也進入了if語句所以它也會例項化Singleton物件。這樣就導致了例項化了兩個Singleton物件。所以單例模式的懶漢式是存線上程安全問題的,既然它存在問題,那麼可能有解決這個問題的方法,那麼究竟怎麼解決呢?對這種問題可能很多人會想到加鎖於是出現了下面這種寫法。

 

4.4懶漢式執行緒安全的[執行緒安全,效率低不推薦使用]

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

缺點:效率太低了,每個執行緒在想獲得類的例項時候,執行getInstance()方法都要進行同步。而其實這個方法只執行一次例項化程式碼就夠了,後面的想獲得該類例項,直接return就行了。方法進行同步效率太低要改進。

4.5單例模式懶漢式[執行緒不安全,不可用]

對於上述缺陷的改進可能有的人會想到如下的程式碼

  1. public class Singleton7 {
  2.  
  3. private static Singleton instance=null;
  4.  
  5. public static Singleton getInstance() {
  6. if (instance == null) {
  7. synchronized (Singleton.class) {
  8. instance = new Singleton();
  9. }
  10. }
  11. return instance;
  12. }
  13. }

其實這種寫法跟4.3一樣是執行緒不安全的,當一個執行緒還沒有例項化Singleton時另一個執行緒執行到if(instance==null)這個判斷語句時就會進入if語句,雖然加了鎖,但是等到第一個執行緒執行完instance=new Singleton()跳出這個鎖時,另一個進入if語句的執行緒同樣會例項化另外一個Singleton物件,執行緒不安全的原理跟4.3類似。因此這種改進方式並不可行,經過大神們一步一步的探索,寫出了懶漢式的雙重校驗鎖。

 

4.6單例模式懶漢式雙重校驗鎖[推薦用]

  1. public class Singleton {
  2. /**
  3. * 懶漢式變種,屬於懶漢式中最好的寫法,保證了:延遲載入和執行緒安全
  4. */
  5. private static Singleton instance=null;
  6.  
  7. private Singleton() {};
  8.  
  9. public static Singleton getInstance(){
  10. if (instance == null) {
  11. synchronized (Singleton.class) {
  12. if (instance == null) {
  13. instance = new Singleton();
  14. }
  15. }
  16. }
  17. return instance;
  18. }
  19. }

訪問方式

Singleton instance = Singleton.getInstance();

得到這個例項後就可以訪問這個類中的方法了。

Double-Check概念對於多執行緒開發者來說不會陌生,如程式碼中所示,我們進行了兩次if (instance== null)檢查,這樣就可以保    證執行緒安全了。這樣,例項化程式碼只用執行一次,後面再次訪問時,判斷if (instance== null),直接return例項化物件。

優點:執行緒安全;延遲載入;效率較高。

4.7內部類[推薦用]

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

訪問方式

Singleton instance = Singleton.getInstance();

得到這個例項後就可以訪問這個類中的方法了。

    這種方式跟餓漢式方式採用的機制類似,但又有不同。兩者都是採用了類裝載的機制來保證初始化例項時只有一個執行緒。不同

的地方在餓漢式方式是隻要Singleton類被裝載就會例項化,沒有Lazy-Loading的作用,而靜態內部類方式在Singleton類被裝載時

並不會立即例項化,而是在需要例項化時,呼叫getInstance方法,才會裝載SingletonHolder類,從而完成Singleton的例項化。

類的靜態屬性只會在第一次載入類的時候初始化,所以在這裡,JVM幫助我們保證了執行緒的安全性,在類進行初始化時,別的執行緒是

無法進入的。

優點:避免了執行緒不安全,延遲載入,效率高。

4.8列舉[極推薦使用]

  1. public enum SingletonEnum {
  2.  
  3. instance;
  4.  
  5. private SingletonEnum() {}
  6.  
  7. public void method(){
  8. }
  9. }

訪問方式

SingletonEnum.instance.method();

可以看到列舉的書寫非常簡單,訪問也很簡單在這裡SingletonEnum.instance這裡的instance即為SingletonEnum型別的引用所以得到它就可以呼叫列舉中的方法了。

藉助JDK1.5中新增的列舉來實現單例模式。不僅能避免多執行緒同步問題,而且還能防止反序列化重新建立新的物件。可能是因為列舉在JDK1.5中才新增,所以在實際專案開發中,很少見人這麼寫過,這種方式也是最好的一種方式,如果在開發中JDK滿足要求的情況下建議使用這種方式。

5、總結

     在真正的專案開發中一般採用4.1、4.6、4.7、4.8看你最喜歡哪種寫法了,一般情況下這幾種模式是沒有問題的,為了裝逼我一般採用4.6這種寫法,我們經常用的Android-Universal-Image-Loader這個開源專案也是採用的4.6這種寫法,其實最安全的寫法是4.8即列舉,它的實現非常簡單而且最安全可謂很完美,但是可能是因為只支援JDK1.5吧又或者是因為列舉大家不熟悉所以目前使用的人並不多,但是大家可以嘗試下。另外當我們使用反射機制時可能不能保證例項的唯一性,但是列舉始終可以保證唯一性,具體請參考次部落格:http://blog.csdn.net/java2000_net/article/details/3983958但是一般情況下很少遇到這種情況。

6、單例模式的在面試中的問題

 

     單例模式在面試中會常常的被遇到,因為它是考擦一個程式設計師的基礎的紮實程度的,如果說你跟面試官說你做過專案,面試官讓你寫幾個單例設計模式,你寫不出來,你覺著面試官會相信嗎?在面試時一定要認真準備每一次面試,靠忽悠即使你被錄取了,你也很有可能會對這個公司不滿意,好了我們言歸正傳,其實單例設計模式在面試中很少有人會問餓漢式寫法,一般都會問單例設計模式的懶漢式的執行緒安全問題,所以大家一定要充分理解單例模式的執行緒安全的問題,就這幾種模式花點時間,認真學透,面試中遇到任何關於單例模式的問題你都不會害怕是吧。

 

如果發現部落格中有任何問題或者您還有什麼疑問歡迎留言,您的支援是我前進的動力

如果本篇部落格對你有幫助請贊一個或者留個言唄

 

轉載請註明出處:http://blog.csdn.net/dmk877/article/details/50311791