10分鐘徹底瞭解單例模式

10分鐘徹底瞭解單例模式

前言

《設計模式自習室》系列,顧名思義,本系列文章帶你溫習常見的設計模式。主要內容有:

  • 該模式的介紹,包括: 引子、意圖(大白話解釋) 類圖、時序圖(理論規範)
  • 該模式的代碼示例:熟悉該模式的代碼長什麼樣子
  • 該模式的優缺點:模式不是萬金油,不可以濫用模式
  • 該模式的實際使用案例:瞭解它在哪些重要的源碼中被使用

該系列會逐步更新於我的博客和公眾號(博客見文章底部)

也希望各位觀眾老爺能夠關注我的個人公眾號:後端技術漫談,不會錯過精彩好看的文章。

系列文章回顧

可以點擊我的頭像查看全部文章

  • 【設計模式自習室】開篇:為什麼我們要用設計模式?
  • 【設計模式自習室】建造者模式
  • 【設計模式自習室】原型模式

創建型——單例模式

引子

《HEAD FIRST設計模式》中“單例模式”又稱為“單件模式”

對於系統中的某些類來說,只有一個實例很重要。比如大家熟悉的Spring框架中,Controller和Service都默認是單例模式。

如果用生活中的例子舉例,一個系統中可以存在多個打印任務,但是隻能有一個正在工作的任務;一個系統只能有一個窗口管理器或文件系統;一個系統只能有一個計時工具或ID(序號)生成器。

如何保證一個類只有一個實例並且這個實例易於被訪問呢?

答:定義一個全局變量可以確保對象隨時都可以被訪問,但不能防止我們實例化多個對象。一個更好的解決辦法是讓類自身負責保存它的唯一實例。這個類可以保證沒有其他實例被創建,並且它可以提供一個訪問該實例的方法。這就是單例模式的模式動機。

意圖

確保一個類只有一個實例,並提供該實例的全局訪問點。

單例模式的要點有三個:

  • 一是某個類只能有一個實例;
  • 二是它必須自行創建這個實例;
  • 三是它必須自行向整個系統提供這個實例。

使用一個私有構造函數、一個私有靜態變量以及一個公有靜態函數來實現。

私有構造函數保證了不能通過構造函數來創建對象實例,只能通過公有靜態函數返回唯一的私有靜態變量。

類圖

如果看不懂UML類圖,可以先粗略瀏覽下該圖,想深入瞭解的話,可以繼續谷歌,深入學習:

10分鐘徹底瞭解單例模式

單例模式的類圖:

10分鐘徹底瞭解單例模式

時序圖

時序圖(Sequence Diagram)是顯示對象之間交互的圖,這些對象是按時間順序排列的。時序圖中顯示的是參與交互的對象及其對象之間消息交互的順序。

我們可以大致瀏覽下時序圖,如果感興趣的小夥伴可以去深究一下:

10分鐘徹底瞭解單例模式

實現

單例模式有非常多的實現方式,這裡我們從最差的實現方式逐漸過渡到優雅的實現方式(劍指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

 Singleton 

getUniqueInstance

(

)

 {         

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

 Singleton 

getUniqueInstance

()

 

{     

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

 Singleton 

GetInstance

(

)      

    {                                  

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

 Singleton 

getUniqueInstance

()

 

{         

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

 Singleton 

getInstance

()

{         

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

 Singleton 

getUniqueInstance

()

 

{         

return

 SingletonHolder.INSTANCE;     } } /<code>

6. 枚舉實現

這是單例模式的最佳實踐,它實現簡單,並且在面對複雜的序列化或者反射攻擊的時候,能夠防止實例化多次。

<code>

public

 

enum

 Singleton {     INSTANCE;     

private

 String objName;     

public

 String 

getObjName

()

 

{         

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

 Singleton 

getUniqueInstance

()

 

{         

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菜鳥入門實戰系列
  • 爬蟲相關技術文章
  • 後端開發相關技術文章
  • 逸聞趣事/好書分享/個人興趣

個人公眾號:後端技術漫談

10分鐘徹底瞭解單例模式

公眾號:後端技術漫談.jpg

如果文章對你有幫助,不妨收藏,投幣,轉發,在看起來~


分享到:


相關文章: