TikTok 首席安全官:公司伺服器已經與位元組跳動分開
單例模式在程式設計中非常的常見,一般來說,某些類,我們希望在程式執行期間有且只有一個例項,原因可能是該類的建立需要消耗系統過多的資源、花費很多的時間,或者業務上客觀就要求了只能有一個例項。
一個場景就是:我們的應用程式有一些配置檔案,我們希望只在系統啟動的時候讀取這些配置檔案,並將這些配置儲存在記憶體中,以後在程式中使用這些配置檔案資訊的時候不必再重新讀取。
定義:
由於某種需要,要保證一個類在程式的生命週期當中只有一個例項,並且提供該例項的全域性訪問方法。
結構:
一般包含三個要素:
- 私有的靜態的例項物件 private static instance
- 私有的建構函式(保證在該類外部,無法通過new的方式來建立物件例項) private Singleton(){}
- 公有的、靜態的、訪問該例項物件的方法 public static Singleton getInstance(){}
UML類圖:
分類:
單例模式就例項的建立時機來劃分可分為:懶漢式與飢漢式兩種。
舉個日常生活中的例子:
媽媽早上起來為我們做飯吃,飯快做好的時候,一般都會叫我們起床吃飯,這是一般的日常情況。如果飯還沒有好的時候,我們就自己起來了(這時候媽媽還沒有叫我們起床),這種情況在單例模式中稱之為飢漢式(媽媽還沒有叫我們起床,我們自己就起來的,就是外部還沒有呼叫自己,自己的例項就已經建立好了)。如果飯做好了,媽媽叫我們起床之後,我們才慢吞吞的起床,這種情況在單例模式中稱之為懶漢式(飯都做好了,媽媽叫你起床之後,自己才起的,能不懶漢嗎?就是外部對該類的方法發出呼叫之後,該例項才建立的)。
懶漢式:顧名思義懶漢式就是應用剛啟動的時候,並不建立例項,當外部呼叫該類的例項或者該類例項方法的時候,才建立該類的例項。是以時間換空間。
懶漢式的優點:例項在被使用的時候才被建立,可以節省系統資源,體現了延遲載入的思想。
延遲載入:通俗上將就是:一開始的時候不載入資源,一直等到馬上就要使用這個資源的時候,躲不過去了才載入,這樣可以儘可能的節省系統資源。
懶漢式的缺點:由於系統剛啟動時且未被外部呼叫時,例項沒有建立;如果一時間有多個執行緒同時呼叫LazySingleton.getLazyInstance()方法很有可能會產生多個例項。
也就是說下面的懶漢式在多執行緒下,是不能保持單例例項的唯一性的,要想保證多執行緒下的單例例項的唯一性得用同步,同步會導致多執行緒下由於爭奪鎖資源,執行效率不高。
飢漢式:顧名思義懶漢式就是應用剛啟動的時候,不管外部有沒有呼叫該類的例項方法,該類的例項就已經建立好了。以空間換時間。
飢漢式的優點:寫法簡單,在多執行緒下也能保證單例例項的唯一性,不用同步,執行效率高。
飢漢式的缺點:在外部沒有使用到該類的時候,該類的例項就建立了,若該類例項的建立比較消耗系統資源,並且外部一直沒有呼叫該例項,那麼這部分的系統資源的消耗是沒有意義的。
下面是懶漢式單例類的演示程式碼:
/** * 懶漢式單例類 */ public class LazySingleton { //私有化建構函式,防止在該類外部通過new的形式建立例項 private LazySingleton() { System.out.println("生成LazySingleton例項一次!"); } //私有的、靜態的例項,設定為私有的防止外部直接訪問該例項變數,設定為靜態的,說明該例項是LazySingleton型別的唯一的 //若開始時,沒有呼叫訪問例項的方法,那麼例項就不會自己建立 private static LazySingleton lazyInstance = null; //公有的訪問單例例項的方法,當外部呼叫訪問該例項的方法時,例項才被建立 public static LazySingleton getLazyInstance() { //若例項還沒有建立,則建立例項;若例項已經被建立了,則直接返回之前建立的例項,即不會返回2個例項 if (lazyInstance == null) { lazyInstance = new LazySingleton(); } return lazyInstance; } } //下面測試類: public class SingletonTest { public static void main(String[] args) { LazySingleton lazyInstance1 = LazySingleton.getLazyInstance(); LazySingleton lazyInstance2 = LazySingleton.getLazyInstance(); LazySingleton lazyInstance3 = LazySingleton.getLazyInstance(); } }
控制檯輸出:
生成LazySingleton例項一次!
在上面的測試類SingletonTest 裡面,連續呼叫了三次LazySingleton.getLazyInstance()方法,
下面程式碼演示飢漢式單例實現:
public class NoLazySingleton { //私有化建構函式,防止在該類外部通過new的形式建立例項 private NoLazySingleton(){ System.out.println("建立NoLazySingleton例項一次!"); } //私有的、靜態的例項,設定為私有的防止外部直接訪問該例項變數,設定為靜態的,說明該例項是LazySingleton型別的唯一的 //當系統載入NoLazySingleton類檔案的時候,就建立了該類的例項 private static NoLazySingleton instance = new NoLazySingleton(); //公有的訪問單例例項的方法 public static NoLazySingleton getInstance(){ return instance; } } //測試程式碼: 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例項一次!
上面說到了懶漢式在多執行緒環境下面是有問題的,下面演示這個多執行緒環境下很有可能出現的問題:
/** * 懶漢式單例類 */ public class LazySingleton { //為了易於模擬多執行緒下,懶漢式出現的問題,我們在建立例項的建構函式裡面使當前執行緒暫停了50毫秒 private LazySingleton() { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("生成LazySingleton例項一次!"); } private static LazySingleton lazyInstance = null; public static LazySingleton getLazyInstance() { if (lazyInstance == null) { lazyInstance = new LazySingleton(); } return lazyInstance; } }
下面是測試程式碼:我們在測試程式碼裡面 新建了10個執行緒,讓這10個執行緒同時呼叫LazySingleton.getLazyInstance()方法
public class SingletonTest { public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(){ @Override public void run() { LazySingleton.getLazyInstance(); } }.start(); } } }
結果控制檯輸出:
生成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個例項了),這就導致了單例的失效。
那如何將懶漢式單例在多執行緒下正確的發揮作用呢?當然是在訪問單例例項的方法處進行同步了。
下面是執行緒安全的懶漢式單例的實現:
public class SafeLazySingleton { private SafeLazySingleton(){ System.out.println("生成SafeLazySingleton例項一次!"); } private static SafeLazySingleton instance = null; //1.對整個訪問例項的方法進行同步 public synchronized static SafeLazySingleton getInstance(){ if (instance == null) { instance = new SafeLazySingleton(); } return instance; } //2.對必要的程式碼塊進行同步 public static SafeLazySingleton getInstance1(){ if (instance == null) { synchronized (SafeLazySingleton.class){ if (instance == null) { instance = new SafeLazySingleton(); } } } return instance; } }
對方法同步:
上面的實現 在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同步塊,並且用了雙重非空校驗,這保證了懶漢式單例模式在多執行緒環境下的有效性,但這種實現感覺還是不夠好,不夠優雅。
下面介紹一種優雅的多執行緒下單例模式的實現方案:
public class GracefulSingleton { private GracefulSingleton(){ System.out.println("建立GracefulSingleton例項一次!"); } //類級的內部類,也就是靜態的成員式內部類,該內部類的例項與外部類的例項沒有繫結關係,而且只有被呼叫到才會裝載,從而實現了延遲載入 private static class SingletonHoder{ //靜態初始化器,由JVM來保證執行緒安全 private static GracefulSingleton instance = new GracefulSingleton(); } public static GracefulSingleton getInstance(){ return SingletonHoder.instance; } }
上面的實現方案使用一個內部類來維護單例類的例項,當GracefulSingleton被載入的時候,其內部類並不會被初始化,所以可以保證當GracefulSingleton被裝載到JVM的時候,不會例項化單例類,當外部呼叫getInstance方法的時候,才會載入內部類SingletonHoder,從而例項化instance,同時由於例項的建立是在類初始化時完成的,所以天生對多執行緒友好,getInstance方法也不需要進行同步。
單例模式本質上是控制單例類的例項數量只有一個,有些時候我們可能想要某個類特定數量的例項,這種情況可以看做是單例模式的一種擴充套件情況。比如我們希望下面的類SingletonExtend只有三個例項,我們可以利用Map來快取這些例項。
import java.util.HashMap; import java.util.Map; public class SingletonExtend { //裝載SingletonExtend例項的容器 private static final Map<String,SingletonExtend> container = new HashMap<String, SingletonExtend>(); //SingletonExtend類最多擁有的例項數量 private static final int MAX_NUM = 3; //例項容器中元素的key的開始值 private static String CACHE_KEY_PRE = "cache"; private static int initNumber = 1; private SingletonExtend(){ System.out.println("建立SingletonExtend例項1次!"); } //先從容器中獲取例項,若例項不存在,在建立例項,然後將建立好的例項放置在容器中 public static SingletonExtend getInstance(){ String key = CACHE_KEY_PRE+ initNumber; SingletonExtend singletonExtend = container.get(key); if (singletonExtend == null) { singletonExtend = new SingletonExtend(); container.put(key,singletonExtend); } initNumber++; //控制容器中例項的數量 if (initNumber > 3) { initNumber = 1; } return singletonExtend; } public static void main(String[] args) { SingletonExtend instance = SingletonExtend.getInstance(); SingletonExtend instance1 = SingletonExtend.getInstance(); SingletonExtend instance2 = SingletonExtend.getInstance(); SingletonExtend instance3 = SingletonExtend.getInstance(); SingletonExtend instance4 = SingletonExtend.getInstance(); SingletonExtend instance5 = SingletonExtend.getInstance(); SingletonExtend instance6 = SingletonExtend.getInstance(); SingletonExtend instance7 = SingletonExtend.getInstance(); SingletonExtend instance8 = SingletonExtend.getInstance(); SingletonExtend instance9 = SingletonExtend.getInstance(); System.out.println(instance); System.out.println(instance1); System.out.println(instance2); System.out.println(instance3); System.out.println(instance4); System.out.println(instance5); System.out.println(instance6); System.out.println(instance7); System.out.println(instance8); System.out.println(instance9); } }
控制檯輸出:
建立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的例項資料只有三個