高併發下線程安全的單例模式(最全最經典,值得收藏)

作者:mlinge-奮鬥吧

blog.csdn.net/cselmu9/article/details/51366946

在所有的設計模式中,單例模式是我們在項目開發中最為常見的設計模式之一,而單例模式有很多種實現方式,你是否都瞭解呢?高併發下如何保證單例模式的線程安全性呢?如何保證序列化後的單例對象在反序列化後仍然是單例的呢?這些問題在看了本文之後都會一一的告訴你答案,趕快來閱讀吧!

什麼是單例模式?

在文章開始之前我們還是有必要介紹一下什麼是單例模式。單例模式是為確保一個類只有一個實例,併為整個系統提供一個全局訪問點的一種模式方法。

從概念中體現出了單例的一些特點:

  • 在任何情況下,單例類永遠只有一個實例存在
  • 單例需要有能力為整個系統提供這一唯一實例

為了便於讀者更好的理解這些概念,下面給出這麼一段內容敘述:

在計算機系統中,線程池、緩存、日誌對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。這些應用都或多或少具有資源管理器的功能。每臺計算機可以有若干個打印機,但只能有一個Printer Spooler,以避免兩個打印作業同時輸出到打印機中。


每臺計算機可以有若干通信端口,系統應當集中管理這些通信端口,以避免一個通信端口同時被兩個請求同時調用。總之,選擇單例模式就是為了避免不一致狀態,避免政出多頭。

正是由於這個特點,單例對象通常作為程序中的存放配置信息的載體,因為它能保證其他對象讀到一致的信息。

例如在某個服務器程序中,該服務器的配置信息可能存放在數據庫或文件中,這些配置數據由某個單例對象統一讀取,服務進程中的其他對象如果要獲取這些配置信息,只需訪問該單例對象即可。

這種方式極大地簡化了在複雜環境 下,尤其是多線程環境下的配置管理,但是隨著應用場景的不同,也可能帶來一些同步問題。

1、餓漢式單例

餓漢式單例是指在方法調用前,實例就已經創建好了。

高併發下線程安全的單例模式(最全最經典,值得收藏)

以上代碼運行結果:

<code>

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

/<code>

從運行結果可以看出實例變量額hashCode值一致,這說明對象是同一個,餓漢式單例實現了。

2、懶漢式單例

懶漢式單例是指在方法調用獲取實例時才創建實例,因為相對餓漢式顯得“不急迫”,所以被叫做“懶漢模式”。

下面是實現代碼:

<code>

package

 org.mlinge.s02;

public

 

class

 

MySingleton

 

{     

private

 

static

 MySingleton instance = 

null

;     

private

 

MySingleton

()

{}     

public

 

static

 MySingleton 

getInstance

()

 

{         

if

(instance == 

null

){             instance = 

new

 MySingleton();         }         

return

 instance;     } }/<code>

這裡實現了懶漢式的單例,但是熟悉多線程併發編程的朋友應該可以看出,在多線程併發下這樣的實現是無法保證實例實例唯一的,甚至可以說這樣的失效是完全錯誤的,下面我們就來看一下多線程併發下的執行情況,這裡為了看到效果,我們對上面的代碼做一小點修改:


高併發下線程安全的單例模式(最全最經典,值得收藏)

執行結果如下:

<code>

1210420568

1210420568

1935123450

1718900954

1481297610

1863264879

369539795

1210420568

1210420568

602269801

/<code>

從這裡執行結果可以看出,單例的線程安全性並沒有得到保證,那要怎麼解決呢?

3、線程安全的懶漢式單例

要保證線程安全,我們就得需要使用同步鎖機制,下面就來看看我們如何一步步的解決 存在線程安全問題的懶漢式單例(錯誤的單例)。

1.方法中聲明synchronized關鍵字

出現非線程安全問題,是由於多個線程可以同時進入getInstance()方法,那麼只需要對該方法進行synchronized的鎖同步即可:


高併發下線程安全的單例模式(最全最經典,值得收藏)

此時仍然使用前面驗證多線程下執行情況的MyThread類來進行驗證,將其放入到org.mlinge.s03包下運行,執行結果如下:

<code>

1689058373

1689058373

1689058373

1689058373

1689058373

1689058373

1689058373

1689058373

1689058373

1689058373

/<code>

從執行結果上來看,問題已經解決了,但是這種實現方式的運行效率會很低。同步方法效率低,那我們考慮使用同步代碼塊來實現:

2.同步代碼塊實現


高併發下線程安全的單例模式(最全最經典,值得收藏)

這裡的實現能夠保證多線程併發下的線程安全性,但是這樣的實現將全部的代碼都被鎖上了,同樣的效率很低下。

3.針對某些重要的代碼來進行單獨的同步(可能非線程安全)

針對某些重要的代碼進行單獨的同步,而不是全部進行同步,可以極大的提高執行效率,我們來看一下:

高併發下線程安全的單例模式(最全最經典,值得收藏)

此時同樣使用前面驗證多線程下執行情況的MyThread類來進行驗證,將其放入到org.mlinge.s04包下運行,執行結果如下:

<code>

1481297610

397630378

1863264879

1210420568

1935123450

369539795

590202901

1718900954

1689058373

602269801

/<code>

從運行結果來看,這樣的方法進行代碼塊同步,代碼的運行效率是能夠得到提升,但是卻沒能保住線程的安全性。看來還得進一步考慮如何解決此問題。推薦:設計模式內容聚合

4.Double Check Locking 雙檢查鎖機制(推薦)

為了達到線程安全,又能提高代碼執行效率,我們這裡可以採用DCL的雙檢查鎖機制來完成,代碼實現如下:


高併發下線程安全的單例模式(最全最經典,值得收藏)

將前面驗證多線程下執行情況的MyThread類放入到org.mlinge.s05包下運行,執行結果如下:

<code>

369539795

369539795

369539795

369539795

369539795

369539795

369539795

369539795

369539795

369539795

/<code>

從運行結果來看,該中方法保證了多線程併發下的線程安全性。

這裡在聲明變量時使用了volatile關鍵字來保證其線程間的可見性;在同步代碼塊中使用二次檢查,以保證其不被重複實例化。集合其二者,這種實現方式既保證了其高效性,也保證了其線程安全性。

4、使用靜態內置類實現單例模式

DCL解決了多線程併發下的線程安全問題,其實使用其他方式也可以達到同樣的效果,代碼實現如下:

<code>

package

 org.mlinge.s06;

public

 

class

 

MySingleton

 

{          

private

 

static

 

class

 

MySingletonHandler

{         

private

 

static

 MySingleton instance = 

new

 MySingleton();     }      

private

 

MySingleton

()

{}     

public

 

static

 MySingleton 

getInstance

()

 

{          

return

 MySingletonHandler.instance;     } }/<code>

以上代碼就是使用靜態內置類實現了單例模式,這裡將前面驗證多線程下執行情況的MyThread類放入到org.mlinge.s06包下運行,執行結果如下:

<code>

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

/<code>

從運行結果來看,靜態內部類實現的單例在多線程併發下單個實例得到了保證。

5、序列化與反序列化的單例模式實現

靜態內部類雖然保證了單例在多線程併發下的線程安全性,但是在遇到序列化對象時,默認的方式運行得到的結果就是多例的。

代碼實現如下:

<code>

package

 org.mlinge.s07;

import

 java.io.Serializable;

public

 

class

 

MySingleton

 

implements

 

Serializable

 

{     

private

 

static

 

final

 

long

 serialVersionUID = 

1L

;          

private

 

static

 

class

 

MySingletonHandler

{         

private

 

static

 MySingleton instance = 

new

 MySingleton();     }      

private

 

MySingleton

()

{}     

public

 

static

 MySingleton 

getInstance

()

 

{          

return

 MySingletonHandler.instance;     } }/<code>

序列化與反序列化測試代碼:

<code>package org.mlinge.s07;

import

 java.io.File;

import

 java.io.FileInputStream;

import

 java.io.FileNotFoundException;

import

 java.io.FileOutputStream;

import

 java.io.IOException;

import

 java.io.ObjectInputStream;

import

 java.io.ObjectOutputStream;

public

 

class

 

SaveAndReadForSingleton

 

{     

public

 

static

 void main(

String

[] args) {         

MySingleton

 singleton = 

MySingleton

.getInstance();         

File

 file = new 

File

(

"MySingleton.txt"

);         

try

 {             

FileOutputStream

 fos = new 

FileOutputStream

(file);             

ObjectOutputStream

 oos = new 

ObjectOutputStream

(fos);             oos.writeObject(singleton);             fos.close();             oos.close();             

System

.out.

println

(singleton.hashCode());         } 

catch

 (

FileNotFoundException

 e) {              e.printStackTrace();         } 

catch

 (

IOException

 e) {              e.printStackTrace();         }         

try

 {             

FileInputStream

 fis = new 

FileInputStream

(file);             

ObjectInputStream

 ois = new 

ObjectInputStream

(fis);             

MySingleton

 rSingleton = (

MySingleton

) ois.readObject();             fis.close();             ois.close();             

System

.out.

println

(rSingleton.hashCode());         } 

catch

 (

FileNotFoundException

 e) {              e.printStackTrace();         } 

catch

 (

IOException

 e) {              e.printStackTrace();         } 

catch

 (

ClassNotFoundException

 e) {              e.printStackTrace();         }     } }/<code>

運行以上代碼,得到的結果如下:

<code>

865113938

1442407170

/<code>

從結果中我們發現,序列號對象的hashCode和反序列化後得到的對象的hashCode值不一樣,說明反序列化後返回的對象是重新實例化的,單例被破壞了。那怎麼來解決這一問題呢?

解決辦法就是在反序列化的過程中使用readResolve()方法,單例實現的代碼如下:

<code>

package

 org.mlinge.s07;

import

 java.io.ObjectStreamException;

import

 java.io.Serializable;

public

 

class

 

MySingleton

 

implements

 

Serializable

 

{     

private

 

static

 

final

 

long

 serialVersionUID = 

1L

;          

private

 

static

 

class

 

MySingletonHandler

{         

private

 

static

 MySingleton instance = 

new

 MySingleton();     }      

private

 

MySingleton

()

{}     

public

 

static

 MySingleton 

getInstance

()

 

{          

return

 MySingletonHandler.instance;     }          

protected

 Object 

readResolve

()

 

throws

 ObjectStreamException 

{         System.out.println(

"調用了readResolve方法!"

);         

return

 MySingletonHandler.instance;      } }/<code>

再次運行上面的測試代碼,得到的結果如下:

<code>

865113938

調用了readResolve方法!

865113938

/<code>

從運行結果可知,添加readResolve方法後反序列化後得到的實例和序列化前的是同一個實例,單個實例得到了保證。

6、使用static代碼塊實現單例

靜態代碼塊中的代碼在使用類的時候就已經執行了,所以可以應用靜態代碼塊的這個特性的實現單例設計模式。

<code>

package

 org.mlinge.s08;

public

 

class

 

MySingleton

{     

private

 

static

 MySingleton instance = 

null

;     

private

 

MySingleton

()

{}     

static

{         instance = 

new

 MySingleton();     }     

public

 

static

 MySingleton 

getInstance

()

 

{          

return

 instance;     }  }/<code>

測試代碼如下:

<code>

package

 org.mlinge.s08;

public

 

class

 

MyThread

 

extends

 

Thread

{          

public

 

void

 

run

()

 

{          

for

 (

int

 i = 

0

; i 5; i++) {             System.out.println(MySingleton.getInstance().hashCode());         }     }     

public

 

static

 

void

 

main

(String[] args)

 

{          MyThread[] mts = 

new

 MyThread[

3

];         

for

(

int

 i = 

0

 ; i new MyThread();         }         

for

 (

int

 j = 

0

; j /<code>

運行結果如下:

<code>

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

/<code>

從運行結果看,單例的線程安全性得到了保證。

7、使用枚舉數據類型實現單例模式

枚舉enum和靜態代碼塊的特性相似,在使用枚舉時,構造方法會被自動調用,利用這一特性也可以實現單例:

<code>

package

 org.mlinge.s09;

public

 

enum

 EnumFactory{      singletonFactory;     

private

 MySingleton instance;     

private

 

EnumFactory

()

{         instance = 

new

 MySingleton();     }     

public

 MySingleton 

getInstance

()

{         

return

 instance;     } }

class

 

MySingleton

{     

public

 

MySingleton

()

{}  }/<code>

測試代碼如下:

<code>

package

 org.mlinge.s09;

public

 

class

 

MyThread

 

extends

 

Thread

{          

public

 

void

 

run

()

 

{          System.out.println(EnumFactory.singletonFactory.getInstance().hashCode());     }     

public

 

static

 

void

 

main

(String[] args)

 

{          MyThread[] mts = 

new

 MyThread[

10

];         

for

(

int

 i = 

0

 ; i new MyThread();         }         

for

 (

int

 j = 

0

; j /<code>

執行後得到的結果:

<code>

1481297610

1481297610

1481297610

1481297610

1481297610

1481297610

1481297610

1481297610

1481297610

1481297610

/<code>

運行結果表明單例得到了保證,但是這樣寫枚舉類被完全暴露了,據說違反了“職責單一原則”,那我們來看看怎麼進行改造呢。

8、完善使用enum枚舉實現單例模式

不暴露枚舉類實現細節的封裝代碼如下:

<code>

package

 org.mlinge.s10;

public

 

class

 

ClassFactory

{      

private

 

enum

 MyEnumSingleton{         singletonFactory;         

private

 MySingleton instance;         

private

 

MyEnumSingleton

()

{             instance = 

new

 MySingleton();         }         

public

 MySingleton 

getInstance

()

{             

return

 instance;         }     }      

public

 

static

 MySingleton 

getInstance

()

{         

return

 MyEnumSingleton.singletonFactory.getInstance();     } }

class

 

MySingleton

{     

public

 

MySingleton

()

{}  }/<code>

驗證單例實現的代碼如下:

<code>

package

 org.mlinge.s10;

public

 

class

 

MyThread

 

extends

 

Thread

{          

public

 

void

 

run

()

 

{          System.out.println(ClassFactory.getInstance().hashCode());     }     

public

 

static

 

void

 

main

(String[] args)

 

{          MyThread[] mts = 

new

 MyThread[

10

];         

for

(

int

 i = 

0

 ; i new MyThread();         }         

for

 (

int

 j = 

0

; j /<code>

驗證結果:

<code>

1935123450

1935123450

1935123450

1935123450

1935123450

1935123450

1935123450

1935123450

1935123450

1935123450

/<code>

驗證結果表明,完善後的單例實現更為合理。

以上就是本文要介紹的所有單例模式的實現,相信認真閱讀的讀者都已經明白文章開頭所引入的那幾個問題了,祝大家讀得開心:-D!

備註:本文的編寫思路和實例源碼參照《Java多線程編程核心技術》-(高洪巖)一書中第六章的學習案例撰寫。


分享到:


相關文章: