前言
《設計模式自習室》系列,顧名思義,本系列文章帶你溫習常見的設計模式。主要內容有:
- 該模式的介紹,包括: 引子、意圖(大白話解釋) 類圖、時序圖(理論規範)
- 該模式的代碼示例:熟悉該模式的代碼長什麼樣子
- 該模式的優缺點:模式不是萬金油,不可以濫用模式
- 該模式的實際使用案例:瞭解它在哪些重要的源碼中被使用
該系列會逐步更新於我的博客和公眾號(博客見文章底部)
也希望各位觀眾老爺能夠關注我的個人公眾號:後端技術漫談,不會錯過精彩好看的文章。
系列文章回顧
可以點擊我的頭像查看全部文章
- 【設計模式自習室】開篇:為什麼我們要用設計模式?
- 【設計模式自習室】建造者模式
- 【設計模式自習室】原型模式
創建型——單例模式
引子
《HEAD FIRST設計模式》中“單例模式”又稱為“單件模式”
對於系統中的某些類來說,只有一個實例很重要。比如大家熟悉的Spring框架中,Controller和Service都默認是單例模式。
如果用生活中的例子舉例,一個系統中可以存在多個打印任務,但是隻能有一個正在工作的任務;一個系統只能有一個窗口管理器或文件系統;一個系統只能有一個計時工具或ID(序號)生成器。
如何保證一個類只有一個實例並且這個實例易於被訪問呢?
答:定義一個全局變量可以確保對象隨時都可以被訪問,但不能防止我們實例化多個對象。一個更好的解決辦法是讓類自身負責保存它的唯一實例。這個類可以保證沒有其他實例被創建,並且它可以提供一個訪問該實例的方法。這就是單例模式的模式動機。
意圖
確保一個類只有一個實例,並提供該實例的全局訪問點。
單例模式的要點有三個:
- 一是某個類只能有一個實例;
- 二是它必須自行創建這個實例;
- 三是它必須自行向整個系統提供這個實例。
使用一個私有構造函數、一個私有靜態變量以及一個公有靜態函數來實現。
私有構造函數保證了不能通過構造函數來創建對象實例,只能通過公有靜態函數返回唯一的私有靜態變量。
類圖
如果看不懂UML類圖,可以先粗略瀏覽下該圖,想深入瞭解的話,可以繼續谷歌,深入學習:
單例模式的類圖:
時序圖
時序圖(Sequence Diagram)是顯示對象之間交互的圖,這些對象是按時間順序排列的。時序圖中顯示的是參與交互的對象及其對象之間消息交互的順序。
我們可以大致瀏覽下時序圖,如果感興趣的小夥伴可以去深究一下:
實現
單例模式有非常多的實現方式,這裡我們從最差的實現方式逐漸過渡到優雅的實現方式(劍指offer的方式),包括:
- 懶漢式-線程不安全
- 餓漢式-線程安全
- 懶漢式-線程安全
- 懶漢式(延遲實例化)—— 線程安全/雙重校驗 (重要,牢記)
- 靜態內部類實現
- 枚舉實現 (重要,牢記)
每個方式也會詳細解釋下面試可能會問到的問題,方便小夥伴複習。
1. 懶漢式-線程不安全
以下實現中,私有靜態變量 uniqueInstance 被延遲實例化,這樣做的好處是,如果沒有用到該類,那麼就不會實例化 uniqueInstance,從而節約資源。
這個實現在多線程環境下是不安全的,如果多個線程能夠同時進入 if (uniqueInstance == null) ,並且此時 uniqueInstance 為 null,那麼會有多個線程執行 uniqueInstance = new Singleton(); 語句,這將導致實例化多次 uniqueInstance。
<code>public
class
Singleton
{private
static
Singleton uniqueInstance;private
Singleton
() { }public
static
SingletongetUniqueInstance
() {if
(uniqueInstance ==null
) { uniqueInstance =new
Singleton(); }return
uniqueInstance; } } /<code>
2. 餓漢式-線程安全
如此一來,只會實例化一次,作為靜態變量
<code>private
static
Singleton uniqueInstance =new
Singleton(); /<code>
3. 懶漢式(延遲實例化)—— 線程安全
只需要對 getUniqueInstance() 方法加鎖,那麼在一個時間點只能有一個線程能夠進入該方法,從而避免了實例化多次 uniqueInstance。
但是當一個線程進入該方法之後,其它試圖進入該方法的線程都必須等待,即使 uniqueInstance 已經被實例化了。這會讓線程阻塞時間過長,因此該方法有性能問題,不推薦使用。
<code>public
static
synchronized
SingletongetUniqueInstance
()
{if
(uniqueInstance ==null
) { uniqueInstance =new
Singleton(); }return
uniqueInstance; } /<code>
4. 懶漢式(延遲實例化)—— 線程安全/雙重校驗
一.私有化構造函數
二.聲明靜態單例對象
三.構造單例對象之前要加鎖(lock一個靜態的object對象)或者方法上加synchronized。
四.需要兩次檢測單例實例是否已經被構造,分別在鎖之前和鎖之後
使用lock(obj)
<code>public
class
Singleton
{private
Singleton
() {}private
volatile
static
Singleton single;private
static
object
obj=new
object
();public
static
SingletonGetInstance
() {if
(single ==null
) {lock
(obj) {if
(single ==null
) { single =new
Singleton(); } } }return
single; } } /<code>
使用synchronized (Singleton.class)
<code>public
class
Singleton
{private
Singleton
()
{}private
volatile
static
Singleton uniqueInstance;public
static
SingletongetUniqueInstance
()
{if
(uniqueInstance ==null
) {synchronized
(Singleton.
class
) {if
(uniqueInstance ==null
) { uniqueInstance =new
Singleton(); } } }return
uniqueInstance; } } /<code>
面試時可能的提問
0.為何要檢測兩次?
答:如果兩個線程同時執行 if 語句,那麼兩個線程就會同時進入 if 語句塊內。雖然在if語句塊內有加鎖操作,但是兩個線程都會執行 uniqueInstance = new Singleton(); 這條語句,只是先後的問題 ,也就是說會進行兩次實例化,從而產生了兩個實例。因此必須使用雙重校驗鎖,也就是需要使用兩個 if 語句。
1.構造函數能否公有化?
答:不行,單例類的構造函數必須私有化,單例類不能被實例化,單例實例只能靜態調用。
2.lock住的對象為什麼要是object對象,可以是int嗎?
答:不行,鎖住的必須是個引用類型。如果鎖值類型,每個不同的線程在聲明的時候值類型變量的地址都不一樣,那麼上個線程鎖住的東西下個線程進來會認為根本沒鎖。
3.uniqueInstance 採用 volatile 關鍵字修飾
uniqueInstance = new Singleton(); 這段代碼其實是分為三步執行。
<code>分配內存空間 初始化對象 將 uniqueInstance 指向分配的內存地址 /<code>
但是由於 JVM 具有指令重排的特性,有可能執行順序變為了
1-->3-->2<code>public
class
Singleton
{private
volatile
static
Singleton uniqueInstance;private
Singleton
()
{}public
static
SingletongetInstance
()
{if
(uniqueInstance ==null
){synchronized
(Singleton.
class
){if
(uniqueInstance ==null
){ uniqueInstance =new
Singleton(); } } }return
uniqueInstance; } } /<code>
所以B線程檢測到不為null後,直接出去調用該單例,而A還沒有運行完構造函數,導致該單例還沒創建完畢,B調用會報錯!所以必須用volatile防止JVM重排指令
5. 靜態內部類實現
當 Singleton 類加載時,靜態內部類 SingletonHolder 沒有被加載進內存。只有當調用 getUniqueInstance() 方法從而觸發 SingletonHolder.INSTANCE 時 SingletonHolder 才會被加載,此時初始化 INSTANCE 實例。
這種方式不僅具有延遲初始化的好處,而且由虛擬機提供了對線程安全的支持。
<code>public
class
Singleton
{private
Singleton
()
{}private
static
class
SingletonHolder
{private
static
final
Singleton INSTANCE =new
Singleton(); }public
static
SingletongetUniqueInstance
()
{return
SingletonHolder.INSTANCE; } } /<code>
6. 枚舉實現
這是單例模式的最佳實踐,它實現簡單,並且在面對複雜的序列化或者反射攻擊的時候,能夠防止實例化多次。
<code>public
enum
Singleton { INSTANCE;private
String objName;public
StringgetObjName
()
{return
objName; }public
void
setObjName
(String objName)
{this
.objName = objName; }public
static
void
main
(String[] args)
{ Singleton firstSingleton = Singleton.INSTANCE; firstSingleton.setObjName("firstName"
); System.out.println(firstSingleton.getObjName()); Singleton secondSingleton = Singleton.INSTANCE; secondSingleton.setObjName("secondName"
); System.out.println(firstSingleton.getObjName()); System.out.println(secondSingleton.getObjName());try
{ Singleton[] enumConstants = Singleton.
class
.getEnumConstants
();for
(Singleton enumConstant : enumConstants) { System.out.println(enumConstant.getObjName()); } }catch
(Exception e) { e.printStackTrace(); } } } /<code>
為什麼枚舉是單例模式的最好方式?
考慮以下單例模式的實現,該 Singleton 在每次序列化的時候都會創建一個新的實例,為了保證只創建一個實例,必須聲明所有字段都是 transient,並且提供一個 readResolve() 方法。
<code>public
class
Singleton
implements
Serializable
{private
static
Singleton uniqueInstance;private
Singleton
()
{ }public
static
synchronized
SingletongetUniqueInstance
()
{if
(uniqueInstance ==null
) { uniqueInstance =new
Singleton(); }return
uniqueInstance; } } /<code>
如果不使用枚舉來實現單例模式,會出現反射 攻擊,因為通過反射的setAccessible() 方法可以將私有構造函數的訪問級別設置為 public,然後調用構造函數從而實例化對象。
枚舉實現是由 JVM 保證只會實例化一次,因此不會出現上述的反射攻擊。
從上面的討論可以看出,解決序列化和反射攻擊很麻煩,而枚舉實現不會出現這兩種問題,所以說枚舉實現單例模式是最佳實踐。
使用場景舉例
- Logger類,全局唯一,保證你能在每個類裡調用為一個Logger輸出日誌
- Spring:Spring裡很多類都是單例的,也是你理解單例最合適的地方,比如Controller和Service類,默認都是單例的。
- 數據庫連接池對象:你從代碼的任何地方都需要拿到連接池裡的資源。
參考
- http://blog.jobbole.com/109449/
- https://github.com/CyC2018/CS-Notes/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20%20-%20%E5%8D%95%E4%BE%8B.md
- 《HEAD FIRST 設計模式》
- 《劍指offer》
關注我
我是一名後端開發工程師。
主要關注後端開發,數據安全,爬蟲,物聯網,邊緣計算等方向,歡迎交流。
各大平臺都可以找到我
- 微信公眾號:後端技術漫談
- Github:@qqxx6661
- CSDN:@Rude3Knife
- 知乎:@Zhendong
- 簡書:@蠻三刀把刀
- 掘金:@蠻三刀把刀
原創博客主要內容
- Java面試知識點複習全手冊
- 設計模式/數據結構 自習室
- Leetcode/劍指offer 算法題解析
- SpringBoot/SpringCloud菜鳥入門實戰系列
- 爬蟲相關技術文章
- 後端開發相關技術文章
- 逸聞趣事/好書分享/個人興趣
個人公眾號:後端技術漫談
公眾號:後端技術漫談.jpg
如果文章對你有幫助,不妨收藏,投幣,轉發,在看起來~