設計模式學習-使用go實現單例模式

Rick.lz發表於2021-11-04

單例模式

定義

什麼是單例模式:保證一個類僅有一個例項,並提供一個全域性訪問它的全域性訪問點。

例如:在某個伺服器程式中,該伺服器的配置資訊存放在一個檔案中,這些配置資料由一個單例物件統一讀取,然後服務程式中的其他物件再通過這個單例物件獲取這些配置資訊。這樣方便了讀取,同時保證了我們的配置資訊只會初始化一次。

優點

1、在單例模式中,活動的單例只有一個例項,對單例類的所有例項化得到的都是相同的一個例項。這樣就 防止其它物件對自己的例項化,確保所有的物件都訪問一個例項

2、單例模式具有一定的伸縮性,類自己來控制例項化程式,類就在改變例項化程式上有相應的伸縮性

3、提供了對唯一例項的受控訪問

4、由於在系統記憶體中只存在一個物件,因此可以節約系統資源,當需要頻繁建立和銷燬的物件時單例模式無疑可以提高系統的效能

5、允許可變數目的例項

6、避免對共享資源的多重佔用

缺點

1、不適用於變化的物件,如果同一型別的物件總是要在不同的用例場景發生變化,單例就會引起資料的錯誤,不能儲存彼此的狀態

2、由於單利模式中沒有抽象層,因此單例類的擴充套件有很大的困難

3、單例類的職責過重,在一定程度上違背了“單一職責原則”

4、濫用單例將帶來一些負面問題,如為了節省資源將資料庫連線池物件設計為的單例類,可能會導致共享連線池物件的程式過多而出現連線池溢位;如果例項化的物件長時間不被利用,系統會認為是垃圾而被回收,這將導致物件狀態的丟失

適用範圍

我們在專案中使用單例,都是用它來表示一些全域性唯一類,比如:配置資訊類、連線池類、ID生成器類。

程式碼實現

懶漢模式

懶漢模式也就是在需要的時候,才去建立例項物件。懶漢式相對於餓漢式的優勢是支援延遲載入。不過懶漢模式不是執行緒安全的,需要加鎖。

// 使用結構體代替類
type Tool struct {
	Name string
}

// 鎖物件
var lock sync.Mutex

// 建立私有變數
var instance *Tool

// 加鎖保證執行緒安全
func GetInstance() *Tool {
	lock.Lock()
	defer lock.Unlock()
	if instance == nil {
		instance = &Tool{
			Name: "我已經初始化了",
		}
	}
	return instance
}

懶漢式的缺點也很明顯,我們給getInstance()這個方法加了一把鎖,導致這個函式的併發度很低。量化一下的話,併發度是1,也就相當於序列操作了。而這個函式是在單例使用期間,一直會被呼叫。如果這個單例類偶爾會被用到,那這種實現方式還可以接受。但是,如果頻繁地用到,那頻繁加鎖、釋放鎖及併發度低等問題,會導致效能瓶頸,這種實現方式就不可取了。

餓漢模式

餓漢模式的實現方式比較簡單。在類載入的時候,instance靜態例項就已經建立並初始化好了,所以,instance例項的建立過程是執行緒安全的。

var cfg *config

func init() {
	cfg = &config{
		Name: "我被初始化了",
	}
}

type config struct {
	Name string
}

// NewConfig 提供獲取例項的方法
func NewConfig() *config {
	return cfg
}

這種方式就是資源提前初始化,有人會講需要的時候才去初始化,能夠避免資源的浪費,不過也有不同的看法。

1、如果初始化耗時長,那我們最好不要等到真正要用它的時候,才去執行這個耗時長的初始化過程,這會影響到系統的效能(比如,在響應客戶端介面請求的時候,做這個初始化操作,會導致此請求的響應時間變長,甚至超時)。採用餓漢式實現方式,將耗時的初始化操作,提前到程式啟動的時候完成,這樣就能避免在程式執行的時候,再去初始化導致的效能問題。

2、如果例項佔用資源多,按照fail-fast的設計原則(有問題及早暴露),那我們也希望在程式啟動時就將這個例項初始化好。如果資源不夠,就會在程式啟動的時候觸發報錯(比如Java中的 PermGen Space OOM),我們可以立即去修復。這樣也能避免在程式執行一段時間後,突然因為初始化這個例項佔用資源過多,導致系統崩潰,影響系統的可用性。

雙重檢測

餓漢式不支援延遲載入,懶漢式有效能問題,不支援高併發。這裡又引入了一種雙重檢測的方法。

在這種實現方式中,只要instance被建立之後,即便再呼叫getInstance()函式也不會再進入到加鎖邏輯中了。

來看下程式碼的實現

// 使用結構體代替類
type Tool struct {
	Name string
}

//鎖物件
var lock sync.Mutex

var instance *Tool

//第一次判斷不加鎖,第二次加鎖保證執行緒安全,一旦物件建立後,獲取物件就不用加鎖了。
func GetInstance() *Tool {
	if instance == nil {
		lock.Lock()
		if instance == nil {
			instance = &Tool{
				Name: "我是雙重檢測,我已經初始化了",
			}
		}
		lock.Unlock()
	}
	return instance
}

sync.Once

go 中也提供了 sync.Once 這個方法,來控制只執行一次,具體原始碼參見go中sync.Once原始碼解讀

// 使用結構體代替類
type Tool struct {
	Name string
}

var instance *Tool

var once sync.Once

func GetOnceInstance() *Tool {
	once.Do(func() {
		instance = &Tool{
			Name: "我sync.once初始化的,我已經初始化了",
		}
	})
	return instance
}

參考

【單例模式】https://zh.wikipedia.org/wiki/單例模式
【大話設計模式】https://book.douban.com/subject/2334288/
【極客時間】https://time.geekbang.org/column/intro/100039001
【單例模式的優缺點和使用場景】https://www.cnblogs.com/damsoft/p/6105122.html
【單例模式】https://boilingfrog.github.io/2021/11/04/使用go實現單例模式/

相關文章