1.單例模式概述
- 單例模式:確定一個類隻有一個執行個體,并提供一個全局通路點來通路點這個唯一執行個體。
- 三個要點:
- 某個類隻能有一個執行個體(單例)
- 它必須自行建立這個執行個體(交給這個單例内部完成)
- 它必須自行向整個系統提供這個執行個體(提供唯一的全局通路點)
2.單線程代碼實作:
/**
* 單線程單例模式
*
*/
public class Singleton {
//私有的對象執行個體
private static Singleton instance = null;
//私有構造函數
private Singleton() {
}
//全局通路點
public static Singleton GetInstance() {
//如果第一次通路,建立一個執行個體
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
/**
* 測試類
*
*/
public class test {
public static void main(String[] args) {
Singleton s1 = Singleton.GetInstance();
Singleton s2 = Singleton.GetInstance();
System.out.println(s1);
System.out.println(s2);
}
}
多線程同時建立這個執行個體時,會出現建立多個執行個體的情況,如下:
/**
* 單線程單例模式在多線程下出現問題
*
*/
public class Singleton {
//私有的執行個體
private static Singleton instance = null;
//私有構造函數
private Singleton() {
}
//全局通路點
public static Singleton GetInstance() throws InterruptedException {
//如果第一次通路,建立一個執行個體
if(instance == null) {
//為使效果更明顯,通過判斷條件後讓兩個睡眠3秒後,再建立執行個體
Thread.sleep(3000);
instance = new Singleton();
}
return instance;
}
}
/**
* 測試類
*
*/
public class test {
public static void main(String[] args) {
Runnable r = () -> {
Singleton getInstance = null;
try {
getInstance = Singleton.GetInstance();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getInstance);
};
//兩個執行個體的位址不相同,說明建立了兩個執行個體
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
}
3.多線程單例模式
如果我們讓類加載的時候就建立執行個體,靜态變量就被初始化,之後類的私有構造函數會被調用,單例類的唯一執行個體就被建立,這種建立方式就是餓漢式。
public class Singleton {
//靜态變量初始化
private static Singleton instance = new Singleton();
//私有構造函數
private Singleton() {}
//全局通路點
public static Singleton GetInstance() {
return instance;
}
}
如果我們不在單例類加載時将自己執行個體化,而是把執行個體化推遲到
GetInstance()
方法調用的時候建立,這就叫做懶漢式,但在高并發、多線程環境下,如果有多個線程使用多個執行個體對象,懶漢式加載還是有可能建立多個執行個體對象的。
是以我們要使用一些進階語言特有的機制(如Java、C#中的
lock
關鍵字等等)來消除建立多個執行個體對象的可能性。
Java多線程
我們可以在整個
GetInstance()
方法上加鎖,這樣做是正确的,但是很笨重,比如有多個線程要通路
GetInstance()
方法,隻有一個線程能得到執行個體,而其他線程都要上鎖等待,效率比較低。
有一種方法可以解決這個問題:雙重檢查鎖定
/**
* 多線程懶加載
*
*/
public class Singleton {
//私有對象執行個體
private static Singleton instance = null;
//私有構造函數
private Singleton() {}
//全局通路點
public static Singleton GetInstance() {
//檢查執行個體是否存在
if(instance == null) {
//檢查靜态方法的類鎖,互斥的通路臨界區
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
我們為什麼要判斷兩次呢?
第一次判斷:是為了判斷執行個體是否存在,如果不是第一次建立執行個體,直接就傳回建立好的執行個體,效率很好。而之前的直接對整個方法加鎖,不管是否已經建立好執行個體,都要不停的檢查類鎖。
第二次判斷:是為了互斥的通路臨界區,防止建立多個執行個體。
問:餓漢式單例和懶漢式單例的異同?
答:餓漢式單例類在類被加載時就将自己執行個體化,優點在于無須考慮多線程同時通路的問題,可以確定執行個體的唯一性,調用速度和反應時間更快,但是無論系統在運作的時候是否使用該單例對象,類一經加載,單例對象就需要建立,是以系統加載時間可能會加長; 懶漢式單例類則是在第一次單例類執行個體化的時候,因為涉及到雙重檢查鎖定等機制進行控制,資源初始化會耗費較長時間。
C#實作方式:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MultiThread
{
/// <summary>
/// 多線程單例模式示例
/// </summary>
public class Singleton
{
/// <summary>
/// 唯一的執行個體對象,每次通路時重新讀取instance變量的值
/// </summary>
private static volatile Singleton instance = null;
/*volatile修飾:編譯器在編譯代碼的時候會對代碼的順序進行微調,用volatile修飾保證了嚴格意義的順序。
一個定義為volatile的變量是說這變量可能會被意想不到地改變,這樣,編譯器就不會去假設這個變量的值了。
精确地說就是,優化器在用到這個變量時必須每次都小心地重新讀取這個變量的值,而不是使用儲存在寄存器裡的備份。*/
/// <summary>
/// 輔助加鎖對象--注意:Static類型
/// </summary>
private static readonly object lockHelper = new object();
/// <summary>
/// 私有構造函數,防止外部應用使用new方法建立新的執行個體
/// </summary>
private Singleton()
{
}
/// <summary>
/// 擷取唯一的執行個體對象
/// </summary>
/// <returns>唯一的執行個體對象</returns>
public static Singleton GetInstance()
{
if (instance == null) //先判斷對象是否存在,不存在時再加鎖處理--這樣做能夠保證執行效率!
{
lock (lockHelper) //加鎖--建立臨界區
{
if (instance == null) //加鎖後二次判斷,隻允許一個線程判斷--有可能之前已有線程建立該對象
{
instance = new Singleton();
}
}
}
return instance;
}
/// <summary>
/// 單例類的其他接口
/// </summary>
public void ShowMe()
{
Console.WriteLine("Singleton pattern--MultiThread...");
}
}
}