人人都會設計模式:02-單例子模式--Singleton

摘要:人人都會設計模式系統宗旨是以簡潔明了方式讓你明白設計模式,本文介紹了單例設計模式,你真的全方位了解過單例模式嗎?我們拭目以待

版權聲明:本文為博主原創文章,未經博主允許不得轉載

公眾號:TigerChain

歡迎關注

教程簡介

1、閱讀對象 本篇教程適合新手閱讀,老手直接略過

2、教程難度 初級,本人水平有限,文章內容難免會出現問題,如果有問題歡迎指出,謝謝

3、Demo 地址https://github.com/githubchen001/DesignPattern請看 SingleTon 部分

正文

一、什么是單例模式

1、 生活中的單例

一個男人只能有一個媳婦「正常情況」,一個人只能有一張嘴,通常一個公司只有一個 CEO ,一個狼群中只有一個狼王等等

2、程序中的單例

一句話,就是保證一個類僅有一個實例即可「new 一次」,其實好多人都不把單例當作成一個設計模式,只是當作是一個工具類而已,因為它的確很簡單,并且當你面視的時候面視官問你設計模式的時候估計都會說:可以說說你了解的設計模式嗎「單例除外」。雖然很簡單,但是我們還是要掌握和了解它,并且要深層次的了解它

單例模式的定義

單例單例就是單一的實例,單例模式就保證一個類僅有一個實例,并且提供一個可以仿問的全局方法可以訪問它

單例模式的應用

網站的計數器

應用配置

多線程池一般也采用單例去設計

數據庫配置,數據庫連接池

其它等等

單例的特點

不能被外部實例化,只能自己內部實例化自己

單例生成的對象是獨一無二的「節省資源」

單例模式的結構

角色類別說明

Singleton單例類就是一個普通的類

getInstance()一個靜態方法提供類的實例

單例模式的 UML

從上圖我們可以了解到編寫一個單例的基本步驟「我稱之為三步法」

1、成員變量靜態化

2、構造方法私有化

3、實例方法靜態化

簡單的代碼結構就是

class SingleTon{privatestaticSingleToninstance;privateSingleTon(){}publicstaticSingleTon getInstance(){? ? ? ? if(null == instance){instance=newSingleTon();? ? ? ? }returninstance ;? ? }}

在實際開發中,我們按照以上三步法就可以創建出一個單例來「直接用方法套用即可」

二、單例模式舉例

單例模式舉例

比如在一個狼群當中,只有一個狼王,有若干偵察狼、捕獵狼等等,這樣就組成了一個狼群,下面看簡單的 java 代碼「代碼只是用來演示單例模式,參考即可」

先看看狼王單例簡單的 UML

根據 UML 編碼

1、定義一個狼的接口,比如這里是下達任務

publicinterfaceIWolf{voiddoSomting();}

2、定義一個偵察狼,它是放哨和探路的

/**

* 偵察狼

*/publicclassZhenChaLangimplementsIWolf{? ? @OverridepublicvoiddoSomting(){// 執行狼王交行的任務System.out.println(" 去探路");? ? }publicvoidfangShao(){? ? ? ? System.out.println(" 去放哨");? ? }}

3、定義一個捕獵狼,獵羊

/**

* 捕獵狼

*/publicclassBuLieLangimplementsIWolf{@OverridepublicvoiddoSomting(){? ? ? ? System.out.println(" 去獵羊");? ? }}

4、主角狼王上場,統一安排規劃

/**

* 狼王

*/publicclassLangWangimplementsIWolf{privatestaticLangWang langWang ;privateLangWang(){? ? ? ? System.out.println("狼王產生了--構造方法被調用");? ? }publicstaticLangWanggetLangWang(){if(null== langWang){? ? ? ? ? ? langWang =newLangWang() ;? ? ? ? }? ? ? ? System.out.println("狼王對應的地址:"+langWang.toString());returnlangWang ;? ? }publicstaticvoidmain(String args[]){? ? ? ? LangWang.getLangWang().doSomting();? ? ? ? LangWang.getLangWang().buLie();? ? }? ? @OverridepublicvoiddoSomting(){// 安排一些工作給下屬狼 比如偵查狼ZhenChaLang zhenChaLang1 =newZhenChaLang() ;? ? ? ? System.out.print("偵察狼 "+zhenChaLang1.toString());? ? ? ? zhenChaLang1.doSomting();? ? ? ? ZhenChaLang zhenChaLang2 =newZhenChaLang();? ? ? ? System.out.print("偵察狼 "+zhenChaLang2.toString());? ? ? ? zhenChaLang2.fangShao();? ? }publicvoidbuLie(){? ? ? ? BuLieLang buLieLang1 =newBuLieLang() ;? ? ? ? System.out.print("捕獵狼 "+buLieLang1.toString());? ? ? ? buLieLang1.doSomting();? ? ? ? BuLieLang buLieLang2 =newBuLieLang() ;? ? ? ? System.out.print("捕獵狼 "+buLieLang2.toString());? ? ? ? buLieLang1.doSomting();? ? }}

我們可以看到狼王是一個單例的「一個狼群確實只有一個狼王」,下面我們來驗證一下結果

我們可以看到,雖然我們調用了兩次狼王實例方法確實都是同一個狼王「地址是一樣的」,而偵查狼和捕獵狼分別是不同的狼,這就是一個單例的使用,各自體會一下。

上面狼王的例子中我們使用的是非線程安全的懶漢式單例模式,單例模式有好幾種實現方式,下面我們來說說這幾種實現方式

單例模式的幾種實現方式

1、餓漢式

餓漢式單例模式如其名,是一個餓貨,類的實例在類加載的時候就初始化出來「把這一過程當作一個漢堡,也就是說必須要把漢堡提前準備好,餓貨就知道吃」

特點

1、是線程安全的

2、類不是延時加載「直接是類加載的時候就初始化」

優缺點

優點:沒有加鎖,執行效率非常高「其實是以空間來換時間」

缺點:在類加載的時候就會初始化,浪費內存「你知道我要不要使用這個實例嗎,你就給我初始化,太任性了」

演示代碼

public class SingleTon{? ? // 1、成員變量靜態化? 餓漢式直接在類加載的時候就初始化實例privatestaticSingleToninstance=newSingleTon();? ? // 2、構造方法私有化privateSingleTon(){}? ? // 3、實例公有方法靜態化publicstaticSingleTon getInstance(){returninstance ;? ? }}

2、懶漢式線程不安全

懶漢式單例模式,是在我需要的時候才去初始化實例,也就是說在類加載的時候,靜態成員變量是 null 的,只有需要它的時候才去初始化實例,所以懶漢式可以延時加載

特點

1、線程不安全

2、延時初始化類,在我需要的時候「也就調用 getInstance」的時候才去初始化

優缺點

1、優點:延時初始化類,省資源,不想用的時候就不會浪費內存

2、缺點:線程不安全,多線程操作就會有問題

演示代碼

public class SingleTon{? ? // 1、類變量靜態化 類加載的時候是空的,所以不開辟內存privatestaticSingleToninstance= null ;? ? // 2、構造方法私有化,這沒什么好說的privateSingleTon(){}? ? // 3、實例方法公有并且靜態化publicstaticSingleTon getInstance(){? ? ? ? if(null == instance){instance=newSingleTon() ;? ? ? ? }? ? }returninstance ;}

3、懶漢式線程安全

懶漢式線程安全比懶漢式線程不全多了一個線程安全

特點

1、線程安全

2、延時初始化類,在我需要的時候「也就調用 getInstance」的時候才去初始化化

優缺點

1、優點:延時初始化類,省資源,不想用的時候就不會浪費內存,并且線程安全

2、缺點:雖然線程安全,但是加了鎖對性能影響非常大「相當于排隊獲取資源,沒有拿到鎖子就干等」

演示代碼

public class SingleTon{privatestaticSingleToninstance;privateSingleTon(){}? ? // 在這里加一個同步鎖,這樣就保證線程安全了publicstaticsynchronized SingleTon getInstance(){? ? ? ? if(null == instalce){instance=newSingleTon() ;? ? ? ? }returninstance ;? ? }}

4、DCL「雙重檢查鎖:double-checked locking」 單例

如其名,雙檢鎖,這種方式單例模式在多線程的情況下能提高性能

特點

1、線程安全

2、延時初始化類,在我需要的時候「也就調用 getInstance」的時候才去初始化化

優缺點

1、優點:延時初始化類,省資源,不想用的時候就不會浪費內存,并且線程安全,雙重加鎖,多線程仿問性能達到提升「后面詳細說 WHY」

2、缺點:雖然線程安全,但是雙檢鎖會遇到指令重排的問題,導致多線程下失效「后面會說」

演示代碼

publicclass DCLSingleTon {? ? /1、成員變量靜態化/privatestaticDCLSingleTon instance ;/*2、構造方法私有化/privateDCLSingleTon(){}? ? /3、實例方法靜態化/publicstaticDCLSingleTon getInstance(){? ? ? ? if(null == instance){ //第一次檢查? ? ? ? ? ? synchronized (DCLSingleTon.class){ //加鎖? ? ? ? ? ? ? ? if(null == instance){ // 第二次檢查instance=newDCLSingleTon() ;? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }returninstance ;? ? }}

雙檢鎖性能提高

那么這種方式,如何保證線程并且有很好的性能呢,首先安全安全不說了看到 synchronized 關鍵字我們就知道了,這里說一下為什么說性能比 3 中的提高了呢

我們知道線程安全性能主要是出在 synchronized 鎖上,我們只要能保證鎖最小化調用即可

從上面代碼可以看出,只有第一次當 instance 為空的時候,才會去調用 synchronized 中的方法,以后就直接返回 synchronized 實例了,也就說 synchronized 只調用一次,所以在多線程上性能會大大的提升

指令重排引起 DCL 問題

這樣做看起來很不錯,解決了多線程問題并延時加載,并且同步一次性能有了不錯的提升,但是這樣做仍然會有問題,這和 Java 的內存模型有關「這種內存模型可以讓處理器大大的提高執行效率」

如果再深入的說,就要說 JAVA 的內存模型了「這不在本節范圍之內」,大家只要記住,Java 的指令重排會導致多線程問題「單線程不會受影響」,指令排序通俗的說就是代碼執行順序改變了,比如:以下一個簡單的例子「下面代碼只是為了說明問題,并不是真實情況下的代碼」

classA{privatestaticinta,b =0;publicstaticvoidmain(String args[]){? ? ? ? a =1;? ? ? ? b =2;? ? ? ? System.out.print("a = "+a+"b = "+b)? ? }}

如果按照正常情況下肯定結果是 a=1,b=2。但是如果指令排序多線程情況下就有可能會出現 a=0,b=2 ,也就是 a = 1 和 b =2 調用順序反過來了「便于理解,實際比這個復雜多了」,這樣就大概解釋了指令重排,詳細可以看看美團點評技術團隊的Java內存訪問重排序的研究講的還是非常好的

DCL 遇到指令重排出現問題分析

上面的問題要從instance = new SingleTon()這句初始化開始「由于這是很多條指令,JVM 可能會指令重排,也叫亂序執行」,這個過程分成三個步驟

1、給 instance 分配內存

2、然后調用 SingleTon 的構造方法初始化成員變量

3、把 instance 對象指向分配的內存空間(到這一步,那么 instance 肯定就是非空的)

問題:

如果按照 1 2 3 執行順序那么也就存在什么問題,可是實際情況是 2 3 執行順序是不確定的「指令重排序」,這時結果就會成 1 3 2 ,那么問題來了,假如按后者來說,3 剛執行完畢,2 還沒有開始之前,突然被另外一個線程2搶占了,此時 instance 已經非空的「但是卻沒有初始化」,那么線程2會直接返回 instance 去使用,結果就是掛了

好了,既然找到了問題,那么解決辦法有以下兩種

1、不讓 2 3 步驟發生指令排序

2、讓保證初始化 intance 時只有一個線程來操作「就是單線程操作,單線程不會存在排序問題」

解決方案一:不發生指令排序

使用 volatile 關鍵字「Java 5 之后 volatile 就可以禁止對指令重新排序 」,就可以指令不發生重排,修改代碼

public class DCLSingleTon {? ? /1、成員變量靜態化/privatevolatilestaticDCLSingleTon instance ;/*2、構造方法私有化/privateDCLSingleTon(){}? ? /3、實例方法靜態化/publicstaticDCLSingleTon getInstance(){? ? ? ? if(null == instance){ //第一次檢查? ? ? ? ? ? synchronized (DCLSingleTon.class){ //加鎖? ? ? ? ? ? ? ? if(null == instance){ // 第二次檢查instance=newDCLSingleTon() ;? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }returninstance ;? ? }}

當然了,Java 5 之后才能完美的使用 volatile ,那么之前如何解決 DCL 安全問題呢?可以使用 Thread Local ,臨時變量等具體可以看關于 DCL 的講解以及改善雙重鎖定被破壞聲明說的非常的好

解決方案二:靜態內存部類 其實就是我們要說的第 5 種單例模式

利用 classloder 的機制來保證初始化 instance 時只有一個線程。JVM 在類初始化階段會獲取一個鎖,這個鎖可以同步多個線程對同一個類的初始化

修改代碼

public class DCLSingleTon {privateDCLSingleTon(){}staticclass SingleTonHolder{privatestaticfinalDCLSingleToninstance=newDCLSingleTon() ;? ? }publicstaticDCLSingleTon getInstance(){returnSingleTonHolder.instance ;? ? }}

5、靜態內部類單例模式

靜態內部類可以允許指令重排,但是對別的線程是不可見的,那么就想當于單線程指令重排對結果是沒有影響的「這是內存模型的特點」,我們來看一下單線程的執行行時序圖,我們來看SingleTon instence = new SingleTon()這一過程

所以靜態內存類單例,你就可以理解成一個線程把上述過程做完了,所以別的線程看不見,所以不會出現時間排序的問題

只要保證 2 在 4 的前面,那么 2 3 是否重排,對結果都是沒有影響的「在單線程的情況下」

特點

1、線程安全

2、延時初始化類,在我需要的時候「也就調用 getInstance」的時候才去初始化化

優缺點

1、優點:延時初始化類,省資源,不想用的時候就不會浪費內存,并且線程安全,還可以執行其它的靜態方法

2、缺點:--

演示代碼

public class SingleTon {privateSingleTon(){}staticclass SingleTonHolder{privatestaticfinalDCLSingleToninstance=newDCLSingleTon() ;? ? }publicstaticSingleTon getInstance(){returnSingleTonHolder.instance ;? ? }}

6、枚舉類單例

枚舉類單例模式是 《Effective Java》 作者極力推薦的單例的方法

特點

特點也就是檢舉類的特點,我們先看看枚舉類的特點吧,多說無用,我們結合 java 代碼來分析

// 一周的枚舉,這里為了說明問題,只列舉到周三publicenumEnumDemo {? MONDAY,? TUESDAY,? WEDNESDAY ;publicvoiddonSomthing(){}}

以上就是一個簡單的枚舉 Java 類,我們反編譯來看一下它的實現機制是雜樣的,在這里我使用jad來反編譯「當然你也可以使用 javap 來反編譯還能看到二進制」,以上 java 代碼反編譯出來的結果如下:

從以上反編譯出來的代碼圖我們可以看出以下幾點信息:

1、枚舉類類型是 final 的「不可以被繼承」

2、構造方法是私有的「也只能私有,不允許被外部實例化,符合單例」

3、類變量是靜態的

4、沒有延時初始化,隨著類的初始化就初始化了「從上面靜態代碼塊中可以看出」

5、由 4 可以知道枚舉也是線程安全的

以上就是枚舉類的特點,很符合單例模式,并且集成上以上幾種單例模式的優點

優缺點

1、優點:除以上特點優點之外,枚舉類還有兩個優點:寫法簡單、支持序列化和反序列化操作「以上的單例序列化和反序列化會破壞單例模式」、并且反射也不能調用構造方法

2、缺點:--

演示代碼

publicenumEnumSingleTon{INSTACE;//定義一個枚舉原素,代表 EnumSingleTon 一個實例? ? /**? ? * 枚舉中的構造方法只能寫成private或是不寫「不寫默認就是private」,所以枚舉防止外部來實例化對象? ? */

EnumSingleTon(){}

/**? ? * 一些額外的方法? ? */

public void doSometing(){

Log.e("枚舉類單例","這是枚舉單例中的方法") ;

}

}

總結

一般情況下,不建議使用第 2 種和第 3 種懶漢式單例,建議使用第 1 種餓漢式單例,如果項目中明確要使用延時加載那么使用第 5 種靜態內存類的單例,如果有序列化反序列化操作可以使用第 6 種單例模式,如果是其它需求可以使用第 4 種 DCL 單例

三、Android 中的單例模式

1、 InputMethodManager 類

InputMethodManager 就一個服務類「輸入法類」源碼目錄Androidsdk\sources\android-26\android\view\inputmethod,部分代碼如下:

@SystemService(Context.INPUT_METHOD_SERVICE)publicfinalclassInputMethodManager{// 省略若干行代碼...staticInputMethodManager sInstance;// 省略若干行代碼...// 以下是構造方法,沒有聲明權限就是私有的InputMethodManager(Looper looper)throwsServiceNotFoundException {this(IInputMethodManager.Stub.asInterface(? ? ? ? ? ? ? ? ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE)), looper);? ? }// 以下是構造方法,沒有聲明權限就是私有的InputMethodManager(IInputMethodManager service, Looper looper) {? ? ? ? mService = service;? ? ? ? mMainLooper = looper;? ? ? ? mH =newH(looper);? ? ? ? mIInputContext =newControlledInputConnectionWrapper(looper,? ? ? ? ? ? ? ? mDummyInputConnection,this);? ? }publicstaticInputMethodManager getInstance() {synchronized(InputMethodManager.class) {if(sInstance ==null) {try{? ? ? ? ? ? ? ? ? ? sInstance =newInputMethodManager(Looper.getMainLooper());? ? ? ? ? ? ? ? }catch(ServiceNotFoundException e) {thrownewIllegalStateException(e);? ? ? ? ? ? ? ? }? ? ? ? ? ? }returnsInstance;? ? ? ? }? ? }// 省略若干行代碼...}

從上面代碼可以看出,InputMethodManager 是一個典型的--線程安全的懶漢式單例

2、Editable 類

文件目錄:frameworks/base/core/java/android/text/Editable.java 部分代碼如下:

privatestaticEditable.Factory sInstance =newEditable.Factory();/**

* Returns the standard Editable Factory.

*/publicstaticEditable.FactorygetInstance(){returnsInstance;? }

可以看到非常典型的一個餓漢式單例模式

Android 源碼中有非常多的單例模式的例子,這里就一一列舉了,相信你看完上面的介紹絕對可以寫出一個適合自己項目的單例了

到此為止,我們就把單例械說完了,動手試試吧,點贊是一種鼓勵,是一種美德

參考資料:

1、美團點評技術團隊:Java內存訪問重排序的研究

2、雙重鎖定被破壞聲明:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

3、方騰飛 《Java 并發編程的藝術》 第三章 Java 內存模型

版權聲明:本文內容由互聯網用戶自發貢獻,本社區不擁有所有權,也不承擔相關法律責任。如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至:yqgroup@service.aliyun.com進行舉報,并提供相關證據,一經查實,本社區將立刻刪除涉嫌侵權內容。

用云棲社區APP,舒服~

原文鏈接

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

推薦閱讀更多精彩內容