設計模式(Java語言)-單例模式
單例模式,簡而言之就是在整個應用程式裡面有且僅有一個例項,在程式的任何時候,任何地方獲取到的該物件都是同一個物件。單例模式解決了一個全域性的類被頻繁建立和銷燬的,或者每次建立或銷燬都需要消耗大量cpu資源的物件的問題。單例模式總的可以分為懶漢模式和餓漢模式,顧名思義,懶漢模式是一個非常懶的漢子,只要你沒有使用到它,它就永遠不會例項化。餓漢模式的意思就是,漢子非常飢渴,只要在程式的編譯階段就給你分配記憶體,建立好物件。
將懶漢模式和餓漢模式細分,又可以分為:
1、懶漢模式
2、餓漢模式
3、雙檢模式
4、靜態內部類模式
5、列舉模式
不管是用哪一種方式實現的單例模式,其建立流程基本都是一直的:首先將構造方法宣告為private的,這樣就防止直接new出一個新的物件。第二,宣告一個私有的成員變數,即單例物件。第三步,宣告一個public的靜態方法,用於獲取或建立單例物件,外部想要獲取該物件必須通過這個方法獲取。
一、懶漢模式1--執行緒安全
/** * 餓漢模式1 */ public class HungrySingleton1 { private static HungrySingleton1 singleton = new HungrySingleton1(); private HungrySingleton1(){} public static HungrySingleton1 getInstance() { return singleton; } }
這種懶漢模式的優點是實現非常簡單。缺點是並起到懶載入的效果,如果專案沒有使用到這個物件的就會造成資源的浪費。
二、餓漢模式1--執行緒不安全
/** * 懶漢模式1 */ public class LazySingleton1 { private static LazySingleton1 singleton; private LazySingleton1(){} public static LazySingleton1 getInstance() { if (singleton == null) { singleton = new LazySingleton1(); } return singleton; } }
這種寫法雖然實現了懶載入的效果,但是嚴格意義上並不是單例模式,因為在多執行緒的環境下有可能會創建出多個不同的物件,至於為什麼,不懂的可以看一下我之間寫的關於Java記憶體模型的文章。這種寫法只能應用於單執行緒的環境下,侷限性很大。實際中強烈不建議使用這種方法。
三、懶漢模式2--執行緒安全
/** * 懶漢模式2 */ public class LazySingleton2 { private static LazySingleton2 singleton; private LazySingleton2(){} public static synchronized LazySingleton2 getInstance() { if (singleton == null) { singleton = new LazySingleton2(); } return singleton; } }
這種寫法咋看跟上面的方法一樣,這種寫法在方法上添加了 synchronized 關鍵字,這樣就保證了每次只能有一個執行緒進入方法體中,解決了懶漢模式1中出現的問題。這種寫法的優點是實現了懶載入的效果,缺點是效率非常低,當多個執行緒同時獲取例項時,有可能會造成執行緒阻塞的情況。不推薦使用。
懶漢模式3--執行緒不安全
/** * 懶漢模式3 */ public class LazySingleton3 { private static LazySingleton3 singleton; private LazySingleton3(){} public static LazySingleton3 getInstance() { if (singleton == null) { synchronized (LazySingleton3.class) { if (singleton == null) { singleton = new LazySingleton3(); } } } return singleton; } }
這種寫法進行了兩次 singleton == null 的判斷,在實際的應用中當我們呼叫這個方法時,其實99%的機率是例項就已經建立好了,因此第一個 singleton == null 能過濾掉99%的呼叫,不用將方法鎖起來,從而提高了效率。這種方法的優點是實現懶載入的效果,效率和很高。缺點是程式碼設計仍然後缺陷,jvm在為物件分配記憶體和賦值並不是一個原子操作,即 singleton = new LazySingleton3() 這段程式碼在jvm中是由三個步驟實現的,首先jvm會在堆中為物件分配一定的記憶體空間,然後完成物件的初始化工作,然後將記憶體地址指向到物件中。但是,我們知道,jvm在編譯的時候並不總是根據我們編寫的程式碼的順序來執行了,而是根據jvm覺得最優的順序執行(這個過程就叫做指令重排序),所以有可能在執行了步驟1後就執行了步驟3,這時候第二個執行緒進來的發現singleton並不為空,因此就直接返回了該物件,因此造成空指標異常。
四、雙重檢查鎖模式---執行緒安全
/** * 懶漢模式4 */ public class LazySingleton4 { private volatile static LazySingleton4 singleton; private LazySingleton4(){} public static LazySingleton4 getInstance() { if (singleton == null) { synchronized (LazySingleton4.class) { if (singleton == null) { singleton = new LazySingleton4(); } } } return singleton; } }
相較於上面的方式,這種方式只是在成員變數中添加了 volatile 關鍵字,解決了指令重排序的問題,同時確保當前執行緒修改了這個變數時,其他的執行緒能夠及時讀到最新的值。這種方法缺點是寫起來比較複雜,要求程式設計師對jvm比較理解。優點是既保證了執行緒安全,同時也能夠保證了比較高的效率。
五、靜態內部類模式--執行緒安全
/** * 懶漢模式5 */ public class LazySingleton5 { private LazySingleton5(){} private static class Holder { private static final LazySingleton5 INSTANCE = new LazySingleton5(); } public static LazySingleton5 getInstance() { return Holder.INSTANCE; } }
這種寫法實現比較簡單,即實現了懶載入的效果,同時也保證的多執行緒環境下的執行緒安全問題。推薦使用這種方式。
六、列舉模式 -- 執行緒安全
/** * 懶漢模式6 */ public enum LazySingleton6 { INSTANCE }
//使用方法
public class Test {
public static void main(String[] args) {
LazySingleton6 instance = LazySingleton6.INSTANCE;
LazySingleton6 instance1 = LazySingleton6.INSTANCE;
System.out.println(instance == instance1);
}
}
推薦寫法,簡單高效。充分利用列舉類的特性,只定義了一個例項,且列舉類是天然支援多執行緒