單例設計模式完全解析

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

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

①什么是設計模式

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

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

④單例模式有哪些寫法

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

1、什么是設計模式?

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

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

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

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

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

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

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 volatile 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吧又或者是因為枚舉大家不熟悉所以目前使用的人并不多,但是大家可以嘗試下。另外當我們使用反射機制時可能不能保證實例的唯一性,但是枚舉始終可以保證唯一性,具體請參考次博客:blog.csdn.net/java2000_ne…但是一般情況下很少遇到這種情況。

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

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


轉載自:blog.csdn.net/dmk877/arti…

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容