設計模式之單例模式

2022-03-24 07:14:06 字數 4013 閱讀 2617

聊聊單例模式,面試加分題

猶記得之前面xx時,面試官一上來就問你知道哪些設計模式,來手寫乙個單例模式的場景;尷尬的我,只寫了懶漢式餓漢式,對於單例其他的變種一概不知;這次就來彌補下這方面的知識盲區!

餓漢式,從字面上理解就是很餓,一上來就要吃的,那麼它會把吃的先準備好,以滿足它的需求;那麼對應到程式上的表現就為:在類載入的時候就會首先進行例項的初始化,後面如果應用程式需要這個例項的話,就有現成的了,可以直接使用當前的單例物件!

我們來手寫下餓漢式的**:

public class singleton

// 宣告私有構造方法 即在外部類無法通過new 初始化例項

private singleton()

public void dosomething()

}class singletondemo

}

餓漢式的優點:它是執行緒安全的,因為單例物件在類載入的時候就被初始化了,當呼叫單例物件時只需要去把對應的物件賦值給變數即可!

餓漢式的缺點:如果這個類不經常使用,會造成一定的資源浪費!

懶漢式,就是比較懶,每次需要填飽肚子時才會外出覓食;那麼對應到程式層面的理解:當應用程式需要某個物件時,該物件的類就會去建立乙個例項,而不是提前準備好的!

我們來手寫下懶漢式的**:

public class singleton2 

return singleton2;

}// 私有構造器

private singleton2()

public void dosomething()

}class singletondemo2

}

同樣我們看下懶漢式的優點:不會造成資源的浪費

懶漢式的缺點:多執行緒情況下,會有執行緒安全的問題;

上面我們可以看到,餓漢式和懶漢式的唯一區別就是:餓漢式在類載入時就完成了物件的初始化,而懶漢式是在需要初始化的時候再去初始化物件;其實在單執行緒情況下,他們都是執行緒安全的;但是我們寫的**,必須考慮多執行緒情況下的併發問題,那麼懶漢式的這種寫法基本不滿足需求,我們需要做些改造,使得它變得執行緒安全,滿足我們的需求!

我們知道,懶漢式下物件的初始化在併發環境下,可能多個執行緒同時執行到singleton2 == null,從而初始化了多個例項,這就引發了執行緒安全問題!

我們就需要改寫它的初始化方法,我們知道加鎖可以解決一般的執行緒安全問題,synchronized這個關鍵字可以修飾乙個**塊或方法,被其修飾的方法或**塊就被加了鎖;而從某些方面理解,synchronized是個同步鎖,亦是個可重入鎖!哈哈,關於鎖的種類及概念有點多,後面準備寫一篇關於鎖的部落格來總結下;不再發散了,回歸正題

我們來改造下懶漢式的初始化方法如下:

// 對外提供初始化方法

public synchronized static singleton2 initinstance()

return singleton2;

}

我們看下上面的**,初看沒什麼問題是解決了執行緒安全問題;但是由於整個方法都被synchronized修飾,那麼在多執行緒的情況下就增加了執行緒同步的開銷,降低了程式的執行效率;為了改進這個問題,我們將synchronized放入到方法內,實現**塊的同步;改下如下:

// 對外提供初始化方法

public static singleton2 initinstance()

}return singleton2;

}

呃,這樣就滿足了我們的要求了嗎?聰明如你一定發現了,雖然我們將synchronized移到了方法內部,降低了同步的開銷,但是在併發的情況下假設多個執行緒同時執行到if(singleton2 == null)時,依舊會排隊初始化singleton2例項,這樣又會造成新的執行緒安全問題;那麼為了解決這個問題,就出現了大名鼎鼎的「雙重檢測鎖」。我們來看下它的實現,將上述**改寫如下:

// 對外提供初始化方法

public static singleton2 initinstance()

}return singleton2;

}

哈哈,這個雙重即是判斷兩次的意思,並不是加兩把鎖哈;那麼這樣就能行了嗎?初看沒問題啊,但是我們細想之下這樣寫真的沒問題嗎?你寫的**,執行的時候真的會按你想的過程執行嗎?有沒有考慮過指令重排呢?問題就出現在new singleton2()這個**上,這行**不是乙個原子操作!

我們再來回顧下指令重排的大致執行流程:

1.給物件例項分配記憶體空間

2.呼叫物件構造方法,初始化成員變數

3.將構造的物件指向分配的記憶體空間

問題就出在指令重排後,cpu對指令重排的優化上,也就是說上述的三個過程並不是每次都是1-2-3順序執行的,而是也有可能1-3-2;那麼我們試想下併發情況下可能出現的場景,當執行緒a執行到步驟3時,cpu時間片正好輪詢到執行緒b,那麼執行緒b判斷例項已經指向了對應的記憶體空間,不為null就不會 初始化例項了,就得到了乙個未初始化完成的物件,這就導致了問題的誕生!

為了解決這個問題,我們知道還有乙個關鍵字volatile可以完美的解決指令重排,使得非原子性的操作對其他物件是可見的!(volatile關鍵字保障了變數的記憶體的可見性和一致性問題,關於記憶體屏障可以看我之前的一篇文章jmm 記憶體模型知識點**了解)。那麼我們將懶漢式改寫如下:

public class singleton2 

}return singleton2;

}// 私有構造器

private singleton2()

public void dosomething()

}class singletondemo2

}

其實除了上面的單例實現外,還有兩種常見的單例實現

**如下:

public class innerclasssingleton 

// 對外提供的初始化方法

public static innerclasssingleton initinstance()

// 私有構造器

private innerclasssingleton()

public void dosomething()

}class innerclasssingletondemo

}

其實,靜態內部類的方式和餓漢式本質是一樣的,都是根據類載入機制來初始化例項,從而保證單例和執行緒安全的;不同的是靜態內部類的方式是按需構建例項,不會如餓漢式一樣造成資源浪費的問題;所以這個是餓漢式乙個比較好的變種!

列舉是比較推薦的一種單例模式,它是執行緒安全的,且通過反射、序列化以及反序列化都無法破壞它的單例屬性(其他的單例採用私有構造器的實現其實並不安全),至於為什麼呢?這個可以參考部落格:[為什麼要用列舉實現單例模式(避免反射、序列化問題)]

**如下:

public class enumsingleton 

private enumsingleton getinstance()

}// 對外提供的初始化方法

public static enumsingleton initinstance()

// 私有構造器

private enumsingleton()

public void dosomething()

}class enumsingletondemo

}

好,至此我們總結了單例的幾種實現方式;比較推薦的是後面兩種方式,一般懶漢式我們就採用雙重檢測鎖的方式;你可以發散思考下單例的應用場景,例如spring中的bean的初始化就是單例模式的典型應用,或者在訊息中心中使用比較頻繁的短鏈結!

設計模式之單例模式

前一段時間買了一本秦小波寫的 設計模式之禪 網上對這書的評價很高。現在還沒有看很多,但是有些地方頗有感觸,也並不是所有的地方都能看懂,但是會慢慢研究的。自己對於設計模式的感覺就是乙個字 牛!感覺會23種設計模式並且會熟練運用的人,真的就是大師級的牛人了,設計模式是乙個專案主管或者架構師一定要會的東西...

設計模式之單例模式

package com.xie.singleton public class singleton 提供乙個共有的靜態的入口方法 public static singleton getinstance 懶漢式 延遲載入 提供乙個私有的靜態的成員變數,但不做初始化 private static sing...

設計模式之 單例模式

單例模式 singleton 保證乙個類僅有乙個例項,並提供乙個訪問它的全域性訪問點。單例模式 單件模式 使用方法返回唯一的例項 public class singleton private static singleton instance public static singleton geti...