版權宣告:本文為博主原創文章,未經博主允許不得轉載
PS:轉載請註明出處
作者: TigerChain
地址: www.jianshu.com/p/62b2e8962…
本文出自 TigerChain 簡書 Android 設計模式系列
教程簡介
- 1、閱讀物件
本篇教程適合新手閱讀,老手直接略過 - 2、教程難度
初級,本人水平有限,文章內容難免會出現問題,如果有問題歡迎指出,謝謝 - 3、Demo 地址
github.com/githubchen0… 請看 SingleTon 部分
正文
一、什麼是單例模式
1、 生活中的單例
一個男人只能有一個媳婦「正常情況」,一個人只能有一張嘴,通常一個公司只有一個 CEO ,一個狼群中只有一個狼王等等
2、程式中的單例
一句話,就是保證一個類僅有一個例項即可「new 一次」,其實好多人都不把單例當作成一個設計模式,只是當作是一個工具類而已,因為它的確很簡單,並且當你面視的時候面視官問你設計模式的時候估計都會說:可以說說你的你瞭解的設計模式嗎「單例除外」。雖然很簡單,但是我們還是要掌握和了解它
單例模式的定義
單例單例就是單一的例項,單例模式就保證一個類僅有一個例項,並且提供一個可以仿問的全域性方法可以訪問它
單例模式的應用
- 網站的計數器
- 應用配置
- 多執行緒池一般也採用單例去設計
- 資料庫配置,資料庫連線池
- 其它等等
單例的特點
- 不能被外部例項化,只能自己內部例項化自己
- 單例生成的物件是獨一無二的「節省資源」
單例模式的結構
角色 | 類別 | 說明 |
---|---|---|
Singleton | 單例類 | 就是一個普通的類 |
getInstance() | 一個靜態方法 | 提供類的例項 |
單例模式的 UML
從上圖我們可以瞭解到編寫一個單例的基本步驟「我稱之為三步法」
- 1、成員變數靜態化
- 2、構造方法私有化
- 3、例項方法靜態化
簡單的程式碼結構就是
class SingleTon{
private static SingleTon instance ;
private SingleTon(){}
public static SingleTon getInstance(){
if(null == instance){
instance = new SingleTon();
}
return instance ;
}
}複製程式碼
在實際開發中,我們按照以上三步法就可以建立出一個單例來「直接用方法套用即可」
二、單例模式舉例
單例模式舉例
比如在一個狼群當中,只有一個狼王,有若干偵察狼、捕獵狼等等,這樣就組成了一個狼群,下面看簡單的 java 程式碼「程式碼只是用來演示單例模式,參考即可」
先看看狼王單例簡單的 UML
根據 UML 編碼
- 1、定義一個狼的介面,比如這裡是下達任務
public interface IWolf {
void doSomting() ;
}複製程式碼
- 2、定義一個偵察狼,它是放哨和探路的
/**
* 偵察狼
*/
public class ZhenChaLang implements IWolf {
@Override
public void doSomting() {
// 執行狼王交行的任務
System.out.println(" 去探路");
}
public void fangShao(){
System.out.println(" 去放哨");
}
}複製程式碼
- 3、定義一個捕獵狼,獵羊
/**
* 捕獵狼
*/
public class BuLieLang implements IWolf {
@Override
public void doSomting() {
System.out.println(" 去獵羊");
}
}複製程式碼
- 4、主角狼王上場,統一安排規劃
/**
* 狼王
*/
public class LangWang implements IWolf {
private static LangWang langWang ;
private LangWang(){
System.out.println("狼王產生了--構造方法被呼叫");
}
public static LangWang getLangWang(){
if(null == langWang){
langWang = new LangWang() ;
}
System.out.println("狼王對應的地址:"+langWang.toString());
return langWang ;
}
public static void main(String args[]){
LangWang.getLangWang().doSomting();
LangWang.getLangWang().buLie();
}
@Override
public void doSomting() {
// 安排一些工作給下屬狼 比如偵查狼
ZhenChaLang zhenChaLang1 = new ZhenChaLang() ;
System.out.print("偵察狼 "+zhenChaLang1.toString());
zhenChaLang1.doSomting();
ZhenChaLang zhenChaLang2 = new ZhenChaLang();
System.out.print("偵察狼 "+zhenChaLang2.toString());
zhenChaLang2.fangShao();
}
public void buLie(){
BuLieLang buLieLang1 = new BuLieLang() ;
System.out.print("捕獵狼 "+buLieLang1.toString());
buLieLang1.doSomting();
BuLieLang buLieLang2 = new BuLieLang() ;
System.out.print("捕獵狼 "+buLieLang2.toString());
buLieLang1.doSomting();
}
}複製程式碼
我們可以看到狼王是一個單例的「一個狼群確實只有一個狼王」,下面我們來驗證一下結果
我們可以看到,雖然我們呼叫了兩次狼王例項方法確實都是同一個狼王,而不偵查狼和捕獵狼分別是不同的狼,這就是一個單例的使用,各自體會一下。
上面狼王的例子中我們使用的是非執行緒安全的懶漢式單例模式,單例模式有好幾種實現方式,下面我們來說說這幾種實現方式
單例模式的幾種實現方式
1、餓漢式
餓漢式單例模式如其名,是一個餓貨,類的例項在類載入的時候就初始化出來「把這一過程當作一個漢堡,也就是說必須要把漢堡提前準備好,餓貨就知道吃」
特點
- 1、是執行緒安全的
- 2、類不是延時載入「直接是類載入的時候就初始化」
優缺點
優點:
沒有加鎖,執行效率非常高「其實是以空間來換時間」缺點:
在類載入的時候就會初始化,浪費記憶體「你知道我要不要使用這個例項嗎,你就給我初始化,太任性了」
演示程式碼
public class SingleTon{
// 1、成員變數靜態化 餓漢式直接在類載入的時候就初始化例項
private static SingleTon instance = new SingleTon();
// 2、構造方法私有化
private SingleTon(){}
// 3、例項公有方法靜態化
public static SingleTon getInstance(){
return instance ;
}
}複製程式碼
2、懶漢式執行緒不安全
懶漢式單例模式,是在我需要的時候才去初始化例項,也就是說在類載入的時候,靜態成員變數是 null 的,只有需要它的時候才去初始化例項,所以懶漢式可以延時載入
特點
- 1、執行緒不安全
- 2、延時初始化類,在我需要的時候「也就呼叫 getInstance」的時候才去初始化化
優缺點
- 1、
優點:
延時初始化類,省資源,不想用的時候就不會浪費記憶體 - 2、
缺點:
執行緒不安全,多執行緒操作就會有問題
演示程式碼
public class SingleTon{
// 1、類變數靜態化 類載入的時候是空的,所以不開闢記憶體
private static SingleTon instance = null ;
// 2、構造方法私有化,這沒什麼好說的
private SingleTon(){}
// 3、例項方法公有並且靜態化
public static SingleTon getInstance(){
if(null == instance){
instance = new SingleTon() ;
}
}
return instance ;
}複製程式碼
3、懶漢式執行緒安全
懶漢式執行緒安全比懶漢式執行緒不全多了一個執行緒安全
特點
- 1、執行緒安全
- 2、延時初始化類,在我需要的時候「也就呼叫 getInstance」的時候才去初始化化
優缺點
- 1、
優點:
延時初始化類,省資源,不想用的時候就不會浪費記憶體,並且執行緒安全 - 2、
缺點:
雖然執行緒安全,但是加了鎖對效能影響非常大「相當於排隊獲取資源,沒有拿到鎖子就乾等」
演示程式碼
public class SingleTon{
private static SingleTon instance ;
private SingleTon(){}
// 在這裡加一個同步鎖,這樣就保證執行緒安全了
public static synchronized SingleTon getInstance(){
if(null == instalce){
instance = new SingleTon() ;
}
return instance ;
}
}複製程式碼
4、DCL「雙重檢查鎖:double-checked locking」 單例
如其名,雙檢鎖,這種方式單例模式在多執行緒的情況下能提高效能
特點
- 1、執行緒安全
- 2、延時初始化類,在我需要的時候「也就呼叫 getInstance」的時候才去初始化化
優缺點
- 1、
優點:
延時初始化類,省資源,不想用的時候就不會浪費記憶體,並且執行緒安全,雙重加鎖,多執行緒仿問效能達到提升「後面詳細說 WHY」 - 2、
缺點:
雖然執行緒安全,但是於在多執行緒中由於指令重排會有問題「後面會說」
演示程式碼
public class DCLSingleTon {
/**1、成員變數靜態化**/
private static DCLSingleTon instance ;
/**2、構造方法私有化*/
private DCLSingleTon(){}
/**3、例項方法靜態化**/
public static DCLSingleTon getInstance(){
if(null == instance){ //第一次檢查
synchronized (DCLSingleTon.class){ //加鎖
if(null == instance){ // 第二次檢查
instance = new DCLSingleTon() ;
}
}
}
return instance ;
}
}複製程式碼
雙檢鎖效能提高
那麼這種方式,如何保證執行緒並且有很好的效能呢,首先安全安全不說了看到 synchronized 關鍵字我們就知道了,這裡說一下為什麼說效能比 3 中的提高了呢
我們知道執行緒安全效能主要是出在 synchronized 鎖上,我們只要能保證鎖最小化呼叫即可
從上面程式碼可以看出,只有第一次當 instance 為空的時候,才會去呼叫 synchronized 中的方法,以後就直接返回 synchronized 例項了,也就說 synchronized 只呼叫一次,所以在多執行緒上效能會大大的提升
指令重排引起 DCL 問題
這樣做看起來很不錯,解決了多執行緒問題並延時載入,並且同步一次效能有了不錯的提升,但是這樣做仍然會有問題,這和 Java 的記憶體模型有關「這種記憶體模型可以讓處理器大大的提高執行效率」
如果再深入的說,就要說 JAVA 的記憶體模型了「這不在本節範圍之內」,大家只要記住,Java 的指令重排會導致多執行緒問題「單執行緒不會受影響」,指令排序通俗的說就是程式碼執行順序改變了,比如:以下一個簡單的例子「下面程式碼只是為了說明問題,並不是真實情況下的程式碼」
class A{
private static int a,b = 0 ;
public static void main(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、成員變數靜態化**/
private volatile static DCLSingleTon instance ;
/**2、構造方法私有化*/
private DCLSingleTon(){}
/**3、例項方法靜態化**/
public static DCLSingleTon getInstance(){
if(null == instance){ //第一次檢查
synchronized (DCLSingleTon.class){ //加鎖
if(null == instance){ // 第二次檢查
instance = new DCLSingleTon() ;
}
}
}
return instance ;
}
}複製程式碼
當然了,Java 5 之後才能完美的使用 volatile ,那麼之前如何解決 DCL 安全問題呢?可以使用 Thread Local ,臨時變數等具體可以看關於 DCL 的講解以及改善 雙重鎖定被破壞宣告 說的非常的好
解決方案二:靜態記憶體部類 其實就是我們要說的第 5 種單例模式
利用 classloder 的機制來保證初始化 instance 時只有一個執行緒。JVM 在類初始化階段會獲取一個鎖,這個鎖可以同步多個執行緒對同一個類的初始化
修改程式碼
public class DCLSingleTon {
private DCLSingleTon(){}
static class SingleTonHolder{
private static final DCLSingleTon instance = new DCLSingleTon() ;
}
public static DCLSingleTon getInstance(){
return SingleTonHolder.instance ;
}
}複製程式碼
5、靜態內部類單例模式
靜態內部類可以允許指令重排,但是對別的執行緒是不可見的,那麼就想當於單執行緒指令重排對結果是沒有影響的「這是記憶體模型的特點」,我們來一下單執行緒的執行行時序圖,我們來看 SingleTon instence = new SingleTon()
這一過程
所以靜態記憶體類單例,你就可以理解成一個執行緒把上述過程做完了,所以別的執行緒看不見,所以不會出現時間排序的問題
只要保證 2 在 4 的前面,那麼 2 3 是否重排,對結果都是沒有影響的「在單執行緒的情況下」
特點
- 1、執行緒安全
- 2、延時初始化類,在我需要的時候「也就呼叫 getInstance」的時候才去初始化化
優缺點
- 1、
優點:
延時初始化類,省資源,不想用的時候就不會浪費記憶體,並且執行緒安全,還可以執行其它的靜態方法 - 2、
缺點:
--
演示程式碼
public class SingleTon {
private SingleTon(){}
static class SingleTonHolder{
private static final DCLSingleTon instance = new DCLSingleTon() ;
}
public static SingleTon getInstance(){
return SingleTonHolder.instance ;
}
}複製程式碼
6、列舉類單例
列舉類單例模式是 《Effective Java》 作者極力推薦的單例的方法
特點
特點也就是檢舉類的特點,我們先看看列舉類的特點吧,多說無用,我們結合 java 程式碼來分析
// 一週的列舉,這裡為了說明問題,只列舉到週三
public enum EnumDemo {
MONDAY,
TUESDAY,
WEDNESDAY ;
public void donSomthing(){}
}複製程式碼
以上就是一個簡單的列舉 Java 類,我們反編譯來看一下它的實現機制是雜樣的,在這裡我使用 jad 來反編譯「當然你也可以使用 javap 來反編譯還能看到二制」,以上 java 程式碼反編譯出來的結果如下:
從以上反編譯出來的程式碼圖我們可以看出以下幾點資訊:
- 1、列舉類型別是 final 的「不可以被繼承」
- 2、構造方法是私有的「也只能私有,不允許被外部例項化,符合單例」
- 3、類變數是靜態的
- 4、沒有延時初始化,隨著類的初始化就初始化了「從上面靜態程式碼塊中可以看出」
- 5、由 4 可以知道列舉也是執行緒安全的
以上就是列舉類的特點,很符合單例模式,並且整合上以上幾種單例模式的優點
優缺點
- 1、
優點:
除以上特點優點之外,列舉類還有兩個優點:寫法簡單
、支援序列化和反序列化操作「以上的單例序列化和反序列化會破壞單例模式」
、並且反射也不能呼叫構造方法
- 2、
缺點:
--
演示程式碼
public enum EnumSingleTon {
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)
public final class InputMethodManager {
// 省略若干行程式碼
...
static InputMethodManager sInstance;
// 省略若干行程式碼
...
// 以下是構造方法,沒有宣告許可權就是私有的
InputMethodManager(Looper looper) throws ServiceNotFoundException {
this(IInputMethodManager.Stub.asInterface(
ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE)), looper);
}
// 以下是構造方法,沒有宣告許可權就是私有的
InputMethodManager(IInputMethodManager service, Looper looper) {
mService = service;
mMainLooper = looper;
mH = new H(looper);
mIInputContext = new ControlledInputConnectionWrapper(looper,
mDummyInputConnection, this);
}
public static InputMethodManager getInstance() {
synchronized (InputMethodManager.class) {
if (sInstance == null) {
try {
sInstance = new InputMethodManager(Looper.getMainLooper());
} catch (ServiceNotFoundException e) {
throw new IllegalStateException(e);
}
}
return sInstance;
}
}
// 省略若干行程式碼
...
}複製程式碼
從上面程式碼可以看出,InputMethodManager 是一個典型的-- 執行緒安全的懶漢式單例
2、Editable 類
檔案目錄:frameworks/base/core/java/android/text/Editable.java 部分程式碼如下:
private static Editable.Factory sInstance = new Editable.Factory();
/**
* Returns the standard Editable Factory.
*/
public static Editable.Factory getInstance() {
return sInstance;
}複製程式碼
可以看到非常典型的一個餓漢式單例模式
Android 原始碼中有非常多的單例模式的例子,這裡就一一列舉了,相信你看完上面的介紹絕對可以寫出一個適合自己專案的單例了
到此為止,我們就把單例械說完了,動手試試吧,點贊是一種鼓勵,是一種美德
參考資料:
- 1、美團點評技術團隊:Java記憶體訪問重排序的研究
- 2、雙重鎖被破壞宣告:www.cs.umd.edu/~pugh/java/…
- 3、方騰飛 《Java 併發程式設計的藝術》 第三章 Java 記憶體模型