Java設計模式—單例設計模式(Singleton Pattern)完全解析

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


     相信大家都知道設計模式,聽的最多的也應該是單例設計模式,這種模式也是在開發中用的最多的設計模式,可能有很多人會寫幾種設計模式,那麼你是否知道什麼是設計模式?爲什麼會有單例設計模式即它的作用是什麼?單例模式有哪些寫法?對於這樣的問題,可能有部分童鞋並不能很好的回答,沒關係今天就和大家一起來詳細的學習下單例設計模式,相信通過學習本篇你將對單例設計模式有個詳細的理解。如有謬誤歡迎批評指正,如有疑問歡迎留言。

 

通過本篇博客你將學到以下內容

①什麼是設計模式

②爲什麼會有單例設計模式即它的用處,以及它解決了什麼問題

③怎樣實現單例,即它的設計思想是什麼

④單例模式有哪些寫法

⑤單例模式在面試中要注意哪些事項

 

1、什麼是設計模式?

 

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

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

 

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

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

假如有一個有這麼一個需求,有一個類A和一個類B它們共享配置文件的信息,在這個配置文件中有很多數據如下圖所示

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

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

     

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

 
ConfigFile configFile=new ConfigFile();

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單例模式的餓漢式[可用]

 
public class Singleton {

    private static Singleton instance=new Singleton();

    private Singleton(){};

    public static Singleton getInstance(){

    return instance;

    }

}

訪問方式

 

Singleton instance = Singleton.getInstance();

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

 

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

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

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

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

 
public class Singleton{

    private static Singleton instance = null;

    static {

        instance = new Singleton();

    }

    private Singleton() {};

    public static Singleton getInstance() {

        return instance;

    }

}

訪問方式:

 

Singleton instance = Singleton.getInstance();

 

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

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

4.3單例模式的懶漢式[線程不安全,不可用]

 
    public class Singleton {

        private static Singleton instance=null;

        private Singleton() {};

        public static Singleton getInstance(){

        if(instance==null){

            instance=new Singleton();

        }

        return instance;

        }

    }

 

這種方式是在調用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懶漢式線程安全的[線程安全,效率低不推薦使用]

 

 
public class Singleton {

    private static Singleton instance=null;

    private Singleton() {};

    public static synchronized Singleton getInstance(){

    if(instance==null){

        instance=new Singleton();

    }

    return instance;

    }

}

 

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

4.5單例模式懶漢式[線程不安全,不可用]

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

 
public class Singleton7 {

    private static Singleton instance=null;

    public static Singleton getInstance() {

    if (instance == null) {

        synchronized (Singleton.class) {

        instance = new Singleton();

        }

    }

    return instance;

    }

}

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

 

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

 

 
public class Singleton {

/**

* 懶漢式變種,屬於懶漢式中最好的寫法,保證了:延遲加載和線程安全

*/

    private static Singleton instance=null;

    private Singleton() {};

    public static Singleton getInstance(){

    if (instance == null) {

        synchronized (Singleton.class) {

            if (instance == null) {

            instance = new Singleton();

            }

        }

    }

    return instance;

    }

}

訪問方式

 

Singleton instance = Singleton.getInstance();

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

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

優點:線程安全;延遲加載;效率較高。

4.7內部類[推薦用]

 

 
public class Singleton{


    private Singleton() {};

    private static class SingletonHolder{

        private static Singleton instance=new Singleton();

    }

    public static Singleton getInstance(){

    return SingletonHolder.instance;

    }

}

 

 

訪問方式

 

Singleton instance = Singleton.getInstance();

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

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

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

並不會立即實例化,而是在需要實例化時,調用getInstance方法,纔會裝載SingletonHolder類,從而完成Singleton的實例化。

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

無法進入的。

優點:避免了線程不安全,延遲加載,效率高。

4.8枚舉[極推薦使用]

 
public enum SingletonEnum {

    instance;

    private SingletonEnum() {}

    public void method(){

    }

}

訪問方式

 

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

 

參考博客:

https://segmentfault.com/q/1010000003732558

 

http://tianweili.github.io/blog/2015/03/02/singleton-pattern/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章