本文簡單介紹了單例設計模式的幾種實現方式,除了列舉單例,其他的所有實現都可以通過反射破壞單例模式,在《effective java》中推薦列舉實現單例模式,在實際場景中使用哪一種單例
2021-06-09 13:30:02
本文簡單介紹了單例設計模式的幾種實現方式,除了列舉單例,其他的所有實現都可以通過反射破壞單例模式,在《effective java》中推薦列舉實現單例模式,在實際場景中使用哪一種單例實現,需要根據自己的情況選擇,適合當前場景的才是比較好的方式。
一、概述
單例模式是面試中經常會被問到的一個問題,網上有大量的文章介紹單例模式的實現,本文也是參考那些優秀的文章來做一個總結,通過自己在學習過程中的理解進行記錄,並補充完善一些內容,一方面鞏固自己所學的內容,另一方面希望能對其他同學提供一些幫助。
本文主要從以下幾個方面介紹單例模式:
單例模式是什麼單例模式的使用場景單例模式的優缺點單例模式的實現(重點)總結二、單例模式是什麼
23 種設計模式可以分為三大類:創建型模式、行為型模式、結構型模式。單例模式屬於創建型模式的一種,單例模式是最簡單的設計模式之一:單例模式只涉及一個類,確保在系統中一個類只有一個例項,並提供一個全局訪問入口。許多時候整個系統只需要擁有一個全局物件,這樣有利於我們協調系統整體的行為。
三、單例模式的使用場景
1、 日誌類
日誌類通常作為單例實現,並在所有應用程式元件中提供全局日誌訪問點,而無需在每次執行日誌操作時創建物件。
2、 配置類
將配置類設計為單例實現,比如在某個伺服器程式中,該伺服器的配置資訊存放在一個檔案中,這些配置資料由一個單例物件統一讀取,然後服務程序中的其他物件再通過這個單例物件獲取這些配置資訊,這種方式簡化了在複雜環境下的配置管理。
3、工廠類
假設我們設計了一個帶有工廠的應用程式,以在多執行緒環境中生成帶有 ID 的新物件(Acount、Customer、Site、Address 物件)。如果工廠在 2 個不同的執行緒中被例項化兩次,那麼 2 個不同的物件可能有 2 個重疊的 id。如果我們將工廠實現為單例,我們就可以避免這個問題,結合抽象工廠或工廠方法和單例設計模式是一種常見的做法。
4、以共享模式訪問資源的類
比如網站的計數器,一般也是採用單例模式實現,如果你存在多個計數器,每一個使用者的訪問都重新整理計數器的值,這樣的話你的實計數的值是難以同步的。但是如果採用單例模式實現就不會存在這樣的問題,而且還可以避免執行緒安全問題。
5、在Spring中創建的Bean例項預設都是單例模式存在的。
適用場景:
需要生成唯一序列的環境需要頻繁例項化然後銷燬的物件。創建物件時耗時過多或者耗資源過多,但又經常用到的物件。方便資源相互通訊的環境四、單例模式的優缺點
優點:
在記憶體中只有一個物件,節省記憶體空間;避免頻繁的創建銷燬物件,減輕 GC 工作,同時可以提高效能;避免對共享資源的多重佔用,簡化訪問;為整個系統提供一個全局訪問點。缺點:
不適用於變化頻繁的物件;濫用單例將帶來一些負面問題,如為了節省資源將資料庫連線池物件設計為的單例類,可能會導致共享連線池物件的程式過多而出現連線池溢位;如果例項化的物件長時間不被利用,系統會認為該物件是垃圾而被回收,這可能會導致物件狀態的丟失;五、單例模式的實現(重點)
實現單例模式的步驟如下:
私有化構造方法,避免外部類通過 new 創建物件定義一個私有的靜態變數持有自己的類型對外提供一個靜態的公共方法來獲取例項如果實現了序列化介面需要保證反序列化不會重新創建物件1、餓漢式,執行緒安全
餓漢式單例模式,顧名思義,類一載入就創建物件,這種方式比較常用,但容易產生垃圾物件,浪費記憶體空間。
優點:執行緒安全,沒有加鎖,執行效率較高缺點:不是懶載入,類載入時就初始化,浪費記憶體空間
懶載入 (lazy loading):使用的時候再創建物件
餓漢式單例是如何保證執行緒安全的呢?它是基於類載入機制避免了多執行緒的同步問題,但是如果類被不同的類載入器載入就會創建不同的例項。
程式碼實現,以及使用反射破壞單例:
使用反射破壞單例,程式碼如下:
輸出結果如下:
2、懶漢式,執行緒不安全
這種方式在單執行緒下使用沒有問題,對於多執行緒是無法保證單例的,這裡列出來是為了和後面使用鎖保證執行緒安全的單例做對比。
優點:懶載入缺點:執行緒不安全
程式碼實現如下:
使用多執行緒破壞單例,測試程式碼如下:
3、懶漢式,執行緒安全
懶漢式單例如何保證執行緒安全呢?通過 synchronized 關鍵字加鎖保證執行緒安全,synchronized 可以新增在方法上面,也可以新增在程式碼塊上面,這裡演示新增在方法上面,存在的問題是每一次呼叫 getInstance 獲取例項時都需要加鎖和釋放鎖,這樣是非常影響效能的。
優點:懶載入,執行緒安全缺點:效率較低
程式碼實現
如下:
4、雙重檢查鎖(DCL, 即 double-checked locking)
實現程式碼如下:
優點:懶載入,執行緒安全,效率較高缺點:實現較複雜
這裡的雙重檢查是指兩次非空判斷,鎖指的是 synchronized 加鎖,為什麼要進行雙重判斷,其實很簡單,第一重判斷,如果例項已經存在,那麼就不再需要進行同步操作,而是直接返回這個例項,如果沒有創建,才會進入同步塊,同步塊的目的與之前相同,目的是為了防止有多個執行緒同時呼叫時,導致生成多個例項,有了同步塊,每次只能有一個執行緒呼叫訪問同步塊內容,當第一個搶到鎖的呼叫獲取了例項之後,這個例項就會被創建,之後的所有呼叫都不會進入同步塊,直接在第一重判斷就返回了單例。
關於內部的第二重空判斷的作用,當多個執行緒一起到達鎖位置時,進行鎖競爭,其中一個執行緒獲取鎖,如果是第一次進入則為 null,會進行單例物件的創建,完成後釋放鎖,其他執行緒獲取鎖後就會被空判斷攔截,直接返回已創建的單例物件。
其中最關鍵的一個點就是 volatile 關鍵字的使用,關於 volatile 的詳細介紹可以直接搜尋 volatile 關鍵字即可,有很多寫的非常好的文章,這裡不做詳細介紹,簡單說明一下,雙重檢查鎖中使用 volatile 的兩個重要特性:可見性、禁止指令重排序
這裡為什麼要使用 volatile?
這是因為 new 關鍵字創建物件不是原子操作,創建一個物件會經歷下面的步驟:
在堆記憶體開闢記憶體空間呼叫構造方法,初始化物件引用變數指向堆記憶體空間對應位元組碼指令如下:
為了提高效能,編譯器和處理器常常會對既定的程式碼執行順序進行指令重排序,從源碼到最終執行指令會經歷如下流程:
所以經過指令重排序之後,創建物件的執行順序可能為 1 2 3 或者 1 3 2 ,因此當某個執行緒在亂序運行 1 3 2 指令的時候,引用變數指向堆記憶體空間,這個物件不為 null,但是沒有初始化,其他執行緒有可能這個時候進入了 getInstance 的第一個 if(instance == null) 判斷不為 nulll ,導致錯誤使用了沒有初始化的非 null 例項,這樣的話就會出現異常,這個就是著名的 DCL 失效問題。
當我們在引用變數上面新增 volatile 關鍵字以後,會通過在創建物件指令的前後新增記憶體屏障來禁止指令重排序,就可以避免這個問題,而且對 volatile 修飾的變數的修改對其他任何執行緒都是可見的。
5、靜態內部類
程式碼實現
如下示例:
優點:懶載入,執行緒安全,效率較高,實現簡單
靜態內部類單例是如何實現懶載入的呢?首先,我們先了解下類的載入時機。
虛擬機器規範要求有且只有5種情況必須立即對類進行初始化(載入、驗證、準備需要在此之前開始):
遇到 new、getstatic、putstatic、invokestatic 這4條位元組碼指令時。生成這4條指令最常見的 Java 程式碼場景是:使用 new 關鍵字例項化物件的時候、讀取或設定一個類的靜態欄位(final修飾除外,被final修飾的靜態欄位是常量,已在編譯期把結果放入常量池)的時候,以及呼叫一個類的靜態方法的時候。使用 java.lang.reflect 包方法對類進行反射呼叫的時候。當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()的那個類),虛擬機器會先初始化這個主類。當使用JDK 1.7的動態語言支援時,如果一個 java.lang.invoke.MethodHandle 例項最後的解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic 的方法控制代碼,則需要先觸發這個方法控制代碼所對應的類的初始化。這5種情況被稱為是類的主動引用,注意,這裡《虛擬機器規範》中使用的限定詞是 "有且僅有",那麼,除此之外的所有引用類都不會對類進行初始化,稱為被動引用。靜態內部類就屬於被動引用的情況。
當getInstance()方法被呼叫時,SingleTonHoler才在SingleTon的運行時常量池裡,把符號引用替換為直接引用,這時靜態物件INSTANCE也真正被創建,然後再被getInstance()方法返回出去,這點同餓漢模式。
那麼 INSTANCE 在創建過程中又是如何保證執行緒安全的呢?在《深入理解JAVA虛擬機器》中,有這麼一句話:
虛擬機器會保證一個類的 <clinit>() 方法在多執行緒環境中被正確地加鎖、同步,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的 <clinit>() 方法,其他執行緒都需要阻塞等待,直到活動執行緒執行 <clinit>() 方法完畢。如果在一個類的 <clinit>() 方法中有耗時很長的操作,就可能造成多個程序阻塞(需要注意的是,其他執行緒雖然會被阻塞,但如果執行<clinit>()方法後,其他執行緒喚醒之後不會再次進入<clinit>()方法。同一個載入器下,一個類型只會初始化一次。),在實際應用中,這種阻塞往往是很隱蔽的。
從上面的分析可以看出INSTANCE在創建過程中是執行緒安全的,所以說靜態內部類形式的單例可保證執行緒安全,也能保證單例的唯一性,同時也延遲了單例的例項化。
6、列舉單例
程式碼實現
如下:
優點:簡單,高效,執行緒安全,可以避免通過反射破壞列舉單例
列舉在java中與普通類一樣,都能擁有欄位與方法,而且列舉例項創建是執行緒安全的,在任何情況下,它都是一個單例,可以直接通過如下方式呼叫獲取例項:
使用下面的命令反編譯列舉類
得到如下內容
從列舉的反編譯結果可以看到,INSTANCE 被 static final 修飾,所以可以通過類名直接呼叫,並且創建物件的例項是在靜態程式碼塊中創建的,因為 static 類型的屬性會在類被載入之後被初始化,當一個Java類第一次被真正使用到的時候靜態資源被初始化、Java類的載入和初始化過程都是執行緒安全的,所以創建一個enum類型是執行緒安全的。
通過反射破壞列舉,實現程式碼如下:
運行結果報如下錯誤:
檢視反射創建例項的 newInstance() 方法,有如下判斷:
所以無法通過反射創建列舉的例項。
六、總結
在java中,如果一個Singleton類實現了java.io.Serializable介面,當這個singleton被多次序列化然後反序列化時,就會創建多個Singleton類的例項。為了避免這種情況,應該實現 readResolve 方法。請參閱 javadocs 中的 Serializable () 和 readResolve Method () 。
使用單例設計模式需要注意的點:
多執行緒- 在多執行緒應用程式中必須使用單例時,應特別小心。序列化- 當單例實現 Serializable 介面時,他們必須實現 readResolve 方法以避免有 2 個不同的物件。類載入器- 如果 Singleton 類由 2 個不同的類載入器載入,我們將有 2 個不同的類,每個類載入一個。由類名錶示的全局訪問點- 使用類名獲取單例例項。這是一種訪問它的簡單方法,但它不是很靈活。如果我們需要替換Sigleton類,程式碼中的所有引用都應該相應地改變。
相關文章
本文簡單介紹了單例設計模式的幾種實現方式,除了列舉單例,其他的所有實現都可以通過反射破壞單例模式,在《effective java》中推薦列舉實現單例模式,在實際場景中使用哪一種單例
2021-06-09 13:30:02
大資料文摘授權轉載自機器人大講堂 一款機器人正模仿奧巴馬向你微笑。 模仿人類「尬笑」、「齜牙咧嘴」…… 這款機器人名叫EVA,由來自中國的哥倫比亞大學電腦科學專業
2021-06-09 13:27:16
為了滿足中高階遊戲玩家需求,耕升(Gainward)在去年推出了全新的星極系列。星極系列最大的特色是採用了亮麗的外觀設計,非常顯眼,外觀上用了很多比如電鍍、烤漆等工藝來讓產品異常
2021-06-09 13:23:56
6月7日,高考第一天,湖北武漢,有網友爆料稱在下午的數學考場裡疑似有考生把全國新高考一卷數學試卷裡的題目拍照上傳到了小猿搜題App上,小猿搜題App工作人員接到資訊後截圖舉報。
2021-06-09 13:23:13
張忠謀的擔心,正在發生!如果說光刻機市場,那麼好歹也是三分天下,但是如果是晶片代工,尤其是先進工藝市場,也就只有三星和臺積電這兩個高階玩家,尤其是臺積電,佔據市場56%份額,甚至三
2021-06-09 13:17:29
5月19日的華為全場景智慧生活新品釋出會上,華為智選·德施曼智慧門鎖Pro正式釋出。該門鎖是華為1+8+N全場景戰略中的一員,支援HUAWEI HilinK,此外還增添3D人臉識別解鎖方式,比一
2021-06-09 13:16:15