單例模式在程序設計中非常的常見,一般來說,某些類,我們希望在程序運行期間有且只有一個實例,原因可能是該類的創建需要消耗系統過多的資源、花費很多的時間,或者業務上客觀就要求了只能有一個實例。一個場景就是:我們的應用程序有一些配置文件,我們希望只在系統啟動的時候讀取這些配置文件,並將這些配置保存在內存中,以后在程序中使用這些配置文件信息的時候不必再重新讀取。
定義:
由於某種需要,要保證一個類在程序的生命周期當中只有一個實例,並且提供該實例的全局訪問方法。
結構:
一般包含三個要素:
1.私有的靜態的實例對象 private static instance
2.私有的構造函數(保證在該類外部,無法通過new的方式來創建對象實例) private Singleton(){}
3.公有的、靜態的、訪問該實例對象的方法 public static Singleton getInstance(){}
UML類圖:
分類:
單例模式就實例的創建時機來划分可分為:懶漢式與飢漢式兩種。
舉個日常生活中的例子:
媽媽早上起來為我們做飯吃,飯快做好的時候,一般都會叫我們起床吃飯,這是一般的日常情況。如果飯還沒有好的時候,我們就自己起來了(這時候媽媽還沒有叫我們起床),這種情況在單例模式中稱之為飢漢式(媽媽還沒有叫我們起床,我們自己就起來的,就是外部還沒有調用自己,自己的實例就已經創建好了)。如果飯做好了,媽媽叫我們起床之后,我們才慢吞吞的起床,這種情況在單例模式中稱之為懶漢式(飯都做好了,媽媽叫你起床之后,自己才起的,能不懶漢嗎?就是外部對該類的方法發出調用之后,該實例才建立的)。
懶漢式:顧名思義懶漢式就是應用剛啟動的時候,並不創建實例,當外部調用該類的實例或者該類實例方法的時候,才創建該類的實例。是以時間換空間。
懶漢式的優點:實例在被使用的時候才被創建,可以節省系統資源,體現了延遲加載的思想。
延遲加載:通俗上將就是:一開始的時候不加載資源,一直等到馬上就要使用這個資源的時候,躲不過去了才加載,這樣可以盡可能的節省系統資源。
懶漢式的缺點:由於系統剛啟動時且未被外部調用時,實例沒有創建;如果一時間有多個線程同時調用LazySingleton.getLazyInstance()方法很有可能會產生多個實例。
也就是說下面的懶漢式在多線程下,是不能保持單例實例的唯一性的,要想保證多線程下的單例實例的唯一性得用同步,同步會導致多線程下由於爭奪鎖資源,運行效率不高。
飢漢式:顧名思義懶漢式就是應用剛啟動的時候,不管外部有沒有調用該類的實例方法,該類的實例就已經創建好了。以空間換時間。
飢漢式的優點:寫法簡單,在多線程下也能保證單例實例的唯一性,不用同步,運行效率高。
飢漢式的缺點:在外部沒有使用到該類的時候,該類的實例就創建了,若該類實例的創建比較消耗系統資源,並且外部一直沒有調用該實例,那么這部分的系統資源的消耗是沒有意義的。
下面是懶漢式單例類的演示代碼:
1 package singleton; 2 3 /** 4 * 懶漢式單例類 5 */ 6 public class LazySingleton { 7 8 //私有化構造函數,防止在該類外部通過new的形式創建實例 9 private LazySingleton() { 10 System.out.println("生成LazySingleton實例一次!"); 11 } 12 13 //私有的、靜態的實例,設置為私有的防止外部直接訪問該實例變量,設置為靜態的,說明該實例是LazySingleton類型的唯一的 14 //若開始時,沒有調用訪問實例的方法,那么實例就不會自己創建 15 private static LazySingleton lazyInstance = null; 16 17 //公有的訪問單例實例的方法,當外部調用訪問該實例的方法時,實例才被創建 18 public static LazySingleton getLazyInstance() { 19 //若實例還沒有創建,則創建實例;若實例已經被創建了,則直接返回之前創建的實例,即不會返回2個實例 20 if (lazyInstance == null) { 21 lazyInstance = new LazySingleton(); 22 } 23 return lazyInstance; 24 } 25 }
下面測試類:
1 package singleton; 2 3 4 public class SingletonTest { 5 public static void main(String[] args) { 6 LazySingleton lazyInstance1 = LazySingleton.getLazyInstance(); 7 LazySingleton lazyInstance2 = LazySingleton.getLazyInstance(); 8 LazySingleton lazyInstance3 = LazySingleton.getLazyInstance(); 9 } 10 }
在上面的測試類SingletonTest 里面,連續調用了三次LazySingleton.getLazyInstance()方法,
控制台輸出:
生成LazySingleton實例一次!
下面代碼演示飢漢式單例實現:
1 package singleton; 2 3 public class NoLazySingleton { 4 5 //私有化構造函數,防止在該類外部通過new的形式創建實例 6 private NoLazySingleton(){ 7 System.out.println("創建NoLazySingleton實例一次!"); 8 } 9 10 //私有的、靜態的實例,設置為私有的防止外部直接訪問該實例變量,設置為靜態的,說明該實例是LazySingleton類型的唯一的 11 //當系統加載NoLazySingleton類文件的時候,就創建了該類的實例 12 private static NoLazySingleton instance = new NoLazySingleton(); 13 14 //公有的訪問單例實例的方法 15 public static NoLazySingleton getInstance(){ 16 return instance; 17 } 18 }
測試代碼:
package singleton; public class SingletonTest { public static void main(String[] args) { NoLazySingleton instance = NoLazySingleton.getInstance(); NoLazySingleton instance1 = NoLazySingleton.getInstance(); NoLazySingleton instanc2 = NoLazySingleton.getInstance(); NoLazySingleton instanc3 = NoLazySingleton.getInstance(); } }
控制台輸出:
創建NoLazySingleton實例一次!
上面說到了懶漢式在多線程環境下面是有問題的,下面演示這個多線程環境下很有可能出現的問題:
1 package singleton; 2 3 /** 4 * 懶漢式單例類 5 */ 6 public class LazySingleton { 7 8 //為了易於模擬多線程下,懶漢式出現的問題,我們在創建實例的構造函數里面使當前線程暫停了50毫秒 9 private LazySingleton() { 10 try { 11 Thread.sleep(50); 12 } catch (InterruptedException e) { 13 e.printStackTrace(); 14 } 15 System.out.println("生成LazySingleton實例一次!"); 16 } 17 18 private static LazySingleton lazyInstance = null; 19 20 public static LazySingleton getLazyInstance() { 21 if (lazyInstance == null) { 22 lazyInstance = new LazySingleton(); 23 } 24 return lazyInstance; 25 } 26 }
下面是測試代碼: 我們在測試代碼里面 新建了10個線程,讓這10個線程同時調用LazySingleton.getLazyInstance()方法
1 package singleton; 2 3 public class SingletonTest { 4 public static void main(String[] args) { 5 for (int i = 0; i < 10; i++) { 6 new Thread(){ 7 @Override 8 public void run() { 9 LazySingleton.getLazyInstance(); 10 } 11 }.start(); 12 } 13 } 14 }
結果控制台輸出:
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
生成LazySingleton實例一次!
沒錯,你沒有看錯,控制台輸出了10次,表示懶漢式單例模式在10個線程同時訪問的時候,創建了10個實例,這足以說明懶漢式單例在多線程下已不能保持其實例的唯一性。
那為什么多線程下懶漢式單例會失效?我們下面分析原因:
我們不說這么多的線程,就說2個線程同時訪問上面的懶漢式單例,現在有兩個線程A和B同時訪問LazySingleton.getLazyInstance()方法。
假設A先得到CPU的時間切片,A執行到21行處 if (lazyInstance == null) 時,由於lazyInstance 之前並沒有實例化,所以lazyInstance == null為true,在還沒有執行22行實例創建的時候
此時CPU將執行時間分給了線程B,線程B執行到21行處 if (lazyInstance == null) 時,由於lazyInstance 之前並沒有實例化,所以lazyInstance == null為true,線程B繼續往下執行實例的創建過程,線程B創建完實例之后,返回。
此時CPU將時間切片分給線程A,線程A接着開始執行22行實例的創建,實例創建完之后便返回。由此看線程A和線程B分別創建了一個實例(存在2個實例了),這就導致了單例的失效。
那如何將懶漢式單例在多線程下正確的發揮作用呢?當然是在訪問單例實例的方法處進行同步了
下面是線程安全的懶漢式單例的實現:
1 package singleton; 2 3 4 public class SafeLazySingleton { 5 6 private SafeLazySingleton(){ 7 System.out.println("生成SafeLazySingleton實例一次!"); 8 } 9 10 private static SafeLazySingleton instance = null; 11 //1.對整個訪問實例的方法進行同步 12 public synchronized static SafeLazySingleton getInstance(){ 13 if (instance == null) { 14 instance = new SafeLazySingleton(); 15 } 16 return instance; 17 }
//2.對必要的代碼塊進行同步 18 public static SafeLazySingleton getInstance1(){ 19 if (instance == null) { 20 synchronized (SafeLazySingleton.class){ 21 if (instance == null) { 22 instance = new SafeLazySingleton(); 23 } 24 } 25 } 26 return instance; 27 } 28 }
對方法同步:
上面的實現 在12行對訪問單例實例的整個方法用了synchronized 關鍵字進行方法同步,這個缺點很是明顯,就是鎖的粒度太大,很多線程同時訪問的時候導致阻塞很嚴重。
對代碼塊同步:
在18行的方法getInstance1中,只是對必要的代碼塊使用了synchronized關鍵字,注意由於方法時static靜態的,所以監視器對象是SafeLazySingleton.class
同時我們在19行和21行,使用了實例兩次非空判斷,一次在進入synchronized代碼塊之前,一次在進入synchronized代碼塊之后,這樣做是有深意的。
肯定有小伙伴這樣想:既然19行進行了實例非空判斷了,進入synchronized代碼塊之后就不必再次進行非空判斷了,如果這樣做的話,會導致什么問題?我們來分析一下:
同樣假設我們有兩個線程A和B,A獲取CPU時間片段,在執行到19行時,由於之前沒有實例化,所以instance == null 為true,然后A獲得監視器對象SafeLazySingleton.class的鎖,A進入synchronized代碼塊里面;
與此同時線程B執行到19行,此時線程A還沒有執行實例化動作,所以此時instance == null 為true,B想進入同步塊,但是發現鎖在線程A手里,所以B只能在同步塊外面等待。此時線程A執行實例化動作,實例化結束之后,返回該實例。
隨着線程A退出同步塊,A也釋放了鎖,線程B就獲得了該鎖,若此時不進行第二次非空判斷,會導致線程B也實例化創建一個實例,然后返回自己創建的實例,這就導致了2個線程訪問創建了2個實例,導致單例失效。若進行第二次非空判斷,發現線程A已經創建了實例,instance == null已經不成立了,則直接返回線程A創建的實例,這樣就避免了單例的失效。
有細心的網友會發現即便去掉19行非空判斷,多線程下單例模式一樣有效:
線程A獲取監視器對象的鎖,進入了同步代碼塊,if(instance == null) 成立,然后A創建了一個實例,然后退出同步塊,返回。這時在同步塊外面等待的線程B,獲取了鎖進入同步塊,執行if(instance == null)發現instance已經有值了不再是空了,然后直接退出同步塊,返回。
既然去掉19行,多線程下單例模式一樣有效,那為什么還要有進入同步塊之前的非空判斷(19行)?這應該主要是考慮到多線程下的效率問題:
我們知道使用synchronized關鍵字進行同步,意味着就是獨占鎖,同一時刻只能有一個線程執行同步塊里面的代碼,還要涉及到鎖的爭奪、釋放等問題,是很消耗資源的。單例模式,構造函數只會被調用一次。如果我們不加19行,即不在進入同步塊之前進行非空判斷,如果之前已經有線程創建了該類的實例了,那每次的訪問該實例的方法都會進入同步塊,這會非常的耗費性能.如果進入同步塊之前加上了非空判斷,發現之前已經有線程創建了該類的實例了,那就不必進入同步塊了,直接返回之前創建的實例即可。這樣就基本上解決了線程同步導致的性能問題。
多線程下單例的優雅的解決方案:
上面的實現使用了synchronized同步塊,並且用了雙重非空校驗,這保證了懶漢式單例模式在多線程環境下的有效性,但這種實現感覺還是不夠好,不夠優雅。
下面介紹一種優雅的多線程下單例模式的實現方案:
1 package singleton; 2 3 public class GracefulSingleton { 4 private GracefulSingleton(){ 5 System.out.println("創建GracefulSingleton實例一次!"); 6 } 7
//類級的內部類,也就是靜態的成員式內部類,該內部類的實例與外部類的實例沒有綁定關系,而且只有被調用到才會裝載,從而實現了延遲加載 8 private static class SingletonHoder{
//靜態初始化器,由JVM來保證線程安全 9 private static GracefulSingleton instance = new GracefulSingleton(); 10 } 11 12 public static GracefulSingleton getInstance(){ 13 return SingletonHoder.instance; 14 } 15 }
上面的實現方案使用一個內部類來維護單例類的實例,當GracefulSingleton被加載的時候,其內部類並不會被初始化,所以可以保證當GracefulSingleton被裝載到JVM的時候,不會實例化單例類,當外部調用getInstance方法的時候,才會加載內部類SingletonHoder,從而實例化instance,同時由於實例的建立是在類初始化時完成的,所以天生對多線程友好,getInstance方法也不需要進行同步。
單例模式本質上是控制單例類的實例數量只有一個,有些時候我們可能想要某個類特定數量的實例,這種情況可以看做是單例模式的一種擴展情況。比如我們希望下面的類SingletonExtend只有三個實例,我們可以利用Map來緩存這些實例。
1 package singleton; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 6 public class SingletonExtend { 7 //裝載SingletonExtend實例的容器 8 private static final Map<String,SingletonExtend> container = new HashMap<String, SingletonExtend>(); 9 //SingletonExtend類最多擁有的實例數量 10 private static final int MAX_NUM = 3; 11 //實例容器中元素的key的開始值 12 private static String CACHE_KEY_PRE = "cache"; 13 private static int initNumber = 1; 14 private SingletonExtend(){ 15 System.out.println("創建SingletonExtend實例1次!"); 16 } 17 18 //先從容器中獲取實例,若實例不存在,在創建實例,然后將創建好的實例放置在容器中 19 public static SingletonExtend getInstance(){ 20 String key = CACHE_KEY_PRE+ initNumber; 21 SingletonExtend singletonExtend = container.get(key); 22 if (singletonExtend == null) { 23 singletonExtend = new SingletonExtend(); 24 container.put(key,singletonExtend); 25 } 26 initNumber++; 27 //控制容器中實例的數量 28 if (initNumber > 3) { 29 initNumber = 1; 30 } 31 return singletonExtend; 32 } 33 34 public static void main(String[] args) { 35 SingletonExtend instance = SingletonExtend.getInstance(); 36 SingletonExtend instance1 = SingletonExtend.getInstance(); 37 SingletonExtend instance2 = SingletonExtend.getInstance(); 38 SingletonExtend instance3 = SingletonExtend.getInstance(); 39 SingletonExtend instance4 = SingletonExtend.getInstance(); 40 SingletonExtend instance5 = SingletonExtend.getInstance(); 41 SingletonExtend instance6 = SingletonExtend.getInstance(); 42 SingletonExtend instance7 = SingletonExtend.getInstance(); 43 SingletonExtend instance8 = SingletonExtend.getInstance(); 44 SingletonExtend instance9 = SingletonExtend.getInstance(); 45 System.out.println(instance); 46 System.out.println(instance1); 47 System.out.println(instance2); 48 System.out.println(instance3); 49 System.out.println(instance4); 50 System.out.println(instance5); 51 System.out.println(instance6); 52 System.out.println(instance7); 53 System.out.println(instance8); 54 System.out.println(instance9); 55 } 56 }
控制台輸出:
創建SingletonExtend實例1次!
創建SingletonExtend實例1次!
創建SingletonExtend實例1次!
singleton.SingletonExtend@3a3ee284
singleton.SingletonExtend@768965fb
singleton.SingletonExtend@36867e89
singleton.SingletonExtend@3a3ee284
singleton.SingletonExtend@768965fb
singleton.SingletonExtend@36867e89
singleton.SingletonExtend@3a3ee284
singleton.SingletonExtend@768965fb
singleton.SingletonExtend@36867e89
singleton.SingletonExtend@3a3ee284
從控制台輸出情況可以看到 我們成功的控制了SingletonExtend的實例數據只有三個
下面就單例模式總結一下:
我們講了什么是單例模式,它的結構是怎么樣的,並且給出了單例的類圖,講了單例的分類:懶漢式和飢漢式,分別講了它們在單線程、多線程環境下的實現方式,它們的優點和缺點,以及優雅的單例模式的實現,最后講了單例模式的擴展,小伙伴們你們清楚了嗎?