前言
學習設計模式也是在代碼上精進的必經之路,在閱讀各種框架原始碼時,如果不懂設計模式,就會看得很吃力;在面試時,面試官也總會問:“了解過設計模式嗎?”,如果這個時候你能和面試官多聊上幾句你對設計模式的理解,是非常加分的
本系列以Java為編程語言,從設計模式中最簡單的單例模式開始,介紹常用的設計模式
原始碼
看完有識訓別忘了點個star哦~
設計模式理解與實作 - 專欄
設計模式理解與實作(1)—— 單例模式
導航
- 前言
- 原始碼
- 設計模式理解與實作 - 專欄
- 單例模式
- 作用
- 優點
- 缺點
- 實作
- 餓漢式
- 1. 最常規的餓漢式思路
- 2. 餓漢式 with 列舉
- 懶漢式
- 懶漢式的執行緒安全問題
- 執行緒安全的懶漢式
- 1. 懶漢式 with 同步方法
- 2. 懶漢式 with DCL - 雙重檢驗鎖
- 3. 懶漢式 with 私有靜態內部類
單例模式
我們知道,在Java中,每一個物件都可以稱為一個實體,如:
// 創建一個學生實體 stu1
Student stu1 = new Student();
在普通的類中,其構造方法常是public的,在程式撰寫程序中需要用到一個類時,只需要new 類名(引數);即可
而單例模式,顧名思義,單例,就是單一實體的意思,指在一個程式運行時,一個類只能有一個實體
作用
一個類只能有一個實體,用處都有啥呢?
- 可作為全域唯一的訪問點,用于:
- 計數器
- 創建連接 / 訪問資源,如資料庫,IO操作等
- 要求生成全域唯一序列號的場景
- 減少一個全域類不必要的創建和銷毀造成的開銷
優點
說完用處說好處:
- 由于單例模式一個類只能有一個實體,減少了記憶體的使用,也減少了創建(特別是有些物件的產生需要吃很多資源,如讀配置、創建依賴物件)和銷毀實體帶來的開銷
- 可以避免對資源檔案的多重占用,比如說呼叫一個單例的實體來寫檔案,其他執行緒想要執行此操作就需要等待,而不會沖突
缺點
說完好處說壞處:
- 單例模式一般是沒有介面的,擴展很困難,要變化的話只能改代碼:因為介面對單例沒有意義,單例要求“自行實體化”,而介面、抽象類是不能被實體化的
- 單例模式和單一職責原則是沖突的:一個類應該只做自己職責內的事,而不應該關心自己是否是單例的
實作
說完概念說實作,單例模式常見的實作有餓漢式和懶漢式
*無論是哪種單例模式,其構造方法都是private的,在他人需要獲取該類的單例時,只需呼叫類方法getInstance()即可
餓漢式
餓漢式單例模式將自身的類實體初始化到類私有靜態常量
1. 最常規的餓漢式思路
最常規的餓漢式就像下面的代碼,平平無奇
public class HungryMan {
/**
* 定義靜態常量指向唯一實體
*/
private static final HungryMan instance = new HungryMan();
/**
* 構造方法私有,禁止別人訪問
*/
private HungryMan() {};
/**
* 通過靜態方法來獲取唯一實體
*
* @return
*/
public static HungryMan getInstance() {
return instance;
}
}
2. 餓漢式 with 列舉
我們應知道,每一個列舉型別和定義的列舉變數在JVM中都是唯一的,根據這個特性,我們也可以利用列舉來創建單例模式
用列舉來創建單例模式,優點有:
- 實作超簡單
- 不怕反射和反序列化的破壞
public enum HungryManWithEnum {
INSTANCE;
public void doSomething(){
System.out.println("借助列舉實作餓漢式單例 - 被呼叫啦~");
}
}
我們只定義一個列舉變數INSTANCE,那么INSTANCE就已經是這個類的唯一實體了,在其它類的方法中,我們可以直接使用類名.INSTANCE.列舉實體方法來呼叫單例方法,非常簡單直觀
懶漢式
在餓漢式中,其將其自身的類實體直接初始化到類私有靜態常量,在類被創建的時候,實體也會一起被創建
但有時候類被創建時,我們還沒到使用它的實體的時候,這時生成的實體就是沒有用的,創建實體造成了開銷,也浪費了記憶體空間
有沒有辦法讓程式在呼叫getInstance()方法時才生成被呼叫類的實體呢?有
public class LazyManWithoutDCL {
private static LazyManWithoutDCL instance = null;
private LazyManWithoutDCL(){};
public static LazyManWithoutDCL getInstance(){
if(instance == null){
instance = new LazyManWithoutDCL();
}
return instance;
}
}
我們將實體的生成放到getInstance()方法中,這樣,就能在呼叫getInstance()方法時,才生成實體了,解決了餓漢式的問題
懶漢式的執行緒安全問題
剛實作完懶加載,問題緊接著就來了,像上面的代碼,在串行運行的時候是沒有問題的,但是,在并發的情況下呢?我們來做一個測驗
/**
* 單例模式 - 測驗類
*/
public class Tester {
private static int testCount = 20;
private static CountDownLatch cdl = new CountDownLatch(testCount);
public static void LazyManWithoutDCLTester() throws InterruptedException {
System.out.println("懶漢式不帶DCL測驗開始");
// 開啟20個執行緒,獲取懶漢式不帶DCL的實體并列印其地址
for(int i=0;i<testCount;++i){
new Thread(()->{
System.out.println(LazyManWithoutDCL.getInstance());
cdl.countDown();
}).start();
}
cdl.await();
System.out.println("懶漢式不帶DCL測驗結束");
}
public static void main(String[] args) throws InterruptedException {
LazyManWithoutDCLTester();
}
}
我們開啟20個執行緒,并發地訪問這個單例,來看一下輸出的結果:
懶漢式不帶DCL測驗開始
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@d718472
design_patterns.singleton.LazyManWithoutDCL@d718472
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@a47a58
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
懶漢式不帶DCL測驗結束
觀察輸出的記憶體地址,竟然輸出了三種不一樣的,這說明這個懶漢式的單例模式竟然創建了三個實體,真的太不靠譜了
那么,有解決辦法嗎?
執行緒安全的懶漢式
1. 懶漢式 with 同步方法
不就是不同步出問題了嘛,加個
synchronized就行了~
public static synchronized LazyManWithoutDCL getInstance(){
if(instance == null){
instance = new LazyManWithoutDCL();
}
return instance;
}
確實可以解決問題,不過synchronized鎖的粒度也太大了,這樣子寫的單例運行效率極低
2. 懶漢式 with DCL - 雙重檢驗鎖
鎖的粒度太大,我們就縮小鎖的粒度就好了~
public class LazyManWithDCL {
/**
* 類加載時,先不加載實體
* 使用 volatile 修飾,這樣第二個執行緒才能立刻知道靜態變數是否還是 null
*/
private volatile static LazyManWithDCL instance = null;
/**
* 構造方法私有,禁止別人訪問
*/
private LazyManWithDCL() {};
public static LazyManWithDCL getInstance(){
// 第一重校驗
if(instance == null){
synchronized (LazyManWithDCL.class){
// 拿到鎖后,進行第二重校驗,避免第二個進來的執行緒再次生成實體
if(instance == null){
instance = new LazyManWithDCL();
}
}
}
return instance;
}
}
需要注意的點:
- 如果沒有雙重檢驗,只有一次檢驗,那么有可能:第一個執行緒拿到鎖后令
instance = new Instance();,在快取回寫的程序中,第二個執行緒拿到鎖了,又執行一遍instance = new Instance();…這樣在記憶體中就不是單例了 - 如果靜態變數
instance沒有使用volatile修飾,則第一個執行緒拿到鎖執行完instance = new Instance();后沒有進行快取回寫,第二個執行緒不知道第一個執行緒已經給instance賦值了,看到的還是instance == null,就會繼續執行instance = new Instance();
3. 懶漢式 with 私有靜態內部類
我們知道,一個類在被加載時,才會將類里的內容進行初始化
我們在單例模式類里面再創建一個私有的靜態內部類,這個內部類只能被它的外部類所創建,且只能創建一次(執行緒安全)
而在外部呼叫單例模式類的getInstance()時,由getInstance()去回傳存在內部類里的單例,這樣,在第一次呼叫getInstance()時,內部類才會被加載(懶加載),并創建單例
public class LazyManWithStaticInnerClass {
private static class MyInnerClass{
public static final LazyManWithStaticInnerClass instance = new LazyManWithStaticInnerClass();
}
private LazyManWithStaticInnerClass(){};
public static LazyManWithStaticInnerClass getInstance(){
// 直接呼叫靜態內部類的實體
return MyInnerClass.instance;
}
}
這種方式實作的懶漢式就好像餓漢式+嵌套一樣,實作起來比較簡單
以上就是單例模式的內容和實作啦,有錯誤的地方歡迎在評論區指正哦~
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/205390.html
標籤:其他
下一篇:軟體測驗就是這么回事?!
