一個菜鳥的設計模式之旅,使用 Golang 實現。本節實現單例模式。三個工作者需要各自找到電梯搭乘!電梯只有一個! ...
一個菜鳥的設計模式之旅,文章可能會有不對的地方,懇請大佬指出錯誤。
編程旅途是漫長遙遠的,在不同時刻有不同的感悟,本文會一直更新下去。
程式介紹
本程式實現了單例模式,三個工作者需要各自找到電梯搭乘!電梯只有一個!
PS C:\Users\小能喵喵喵\Desktop\設計模式\單例模式> go run .
向海寧 正在搭乘電梯!
田海彬 正在搭乘電梯!
程式代碼
singleton.go
package main
import (
"fmt"
"sync"
"sync/atomic"
)
type people string
type elevator struct {
passengers map[people]bool
}
var (
Elevator *elevator
initialized uint32
mu sync.Mutex
)
// 餓漢式單例
func init() {
initialized = 1
Elevator = &elevator{make(map[people]bool)}
}
// 乘客進電梯
func (p people) intoElevator() {
e := ElevatorGetInstance()
e.passengers[p] = true
}
// 乘客出電梯
func (p people) outElevator() {
e := ElevatorGetInstance()
delete(e.passengers, p)
}
// 乘客按下電梯樓層按鈕
func (p people) pressButton() {
e := ElevatorGetInstance()
e.start()
}
// 電梯開始運作
func (e *elevator) start() {
for k := range e.passengers {
fmt.Println(k, "正在搭乘電梯!")
}
}
// 乘客想知道電梯在哪,單例模式
func ElevatorGetInstance() *elevator {
if atomic.LoadUint32(&initialized) == 1 {
return Elevator
}
mu.Lock()
defer mu.Unlock()
// ^ 懶漢式單例
if Elevator == nil {
Elevator = &elevator{make(map[people]bool)}
atomic.StoreUint32(&initialized, 1)
}
return Elevator
}
main.go
package main
// 單例模式
// by 小能喵喵喵 2022年9月8日
func main() {
var workerA, workerB, workerC people = "陳冰", "向海寧", "田海彬"
workerA.intoElevator()
workerB.intoElevator()
workerC.intoElevator()
// workerA 發現自己電梯坐錯了
workerA.outElevator()
// workerB 按下了電梯按鈕
workerB.pressButton()
}
Console
PS C:\Users\小能喵喵喵\Desktop\設計模式\單例模式> go run .
向海寧 正在搭乘電梯!
田海彬 正在搭乘電梯!
補充C#單線程單例實現
class Singleton
{
private static Singleton instance;
private Singleton() //使用 private 欄位外界無法使用new手動創建實例,只能通過靜態方法創建
{
}
public static Singleton GetInstance()
{
if(instance == null)
{
instance = new Singleton();
}
return instance
}
}
所有類都有構造方法,不編碼則系統預設生成空的構造方法,若有顯示定義的構造方法,預設的構造方法就會失效。
思考總結
什麼是單例模式
單例模式(Singleton Pattern)屬於創建型模式,它提供了一種創建對象的最佳方式。
單例模式:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。
這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。(由於Golang不支持靜態方法、靜態欄位,所以使用純函數替代面向對象中的靜態方法,即上面程式的ElevatorGetInstance()
函數)
職責角度看,實例化與否不應該由使用方判斷,而是應該由自己來判斷。將實例化判斷過程遷移到GetInstance()
函數。
在C#中的單例模式,Singleton類封裝它的唯一實例,這樣它可以嚴格地控制客戶怎樣訪問它以及何時訪問它。簡單地說就是對唯一實例的受控訪問。當然在golang的實現中,我們也可以更改ElevatorGetInstance()
函數添加條件和參數實現受控訪問。
註意:
- 單例類只能有一個實例。
- 單例類必須自己創建自己的唯一實例。
- 單例類必須給所有其他對象提供這一實例。
主要解決:一個全局使用的類頻繁地創建與銷毀。
何時使用:當您想控制實例數目,節省系統資源的時候。
如何解決:判斷系統是否已經有這個單例,如果有則返回,如果沒有則創建。
關鍵代碼:構造函數是私有的。(c#)
應用實例:
- 1、一個班級只有一個班主任。
- 2、Windows 是多進程多線程的,在操作一個文件的時候,就不可避免地出現多個進程或線程同時操作一個文件的現象,所以所有文件的處理必須通過唯一的實例來進行。
- 3、一些設備管理器常常設計為單例模式,比如一個電腦有兩台印表機,在輸出的時候就要處理不能兩台印表機列印同一個文件。
優點:
- 1、在記憶體里只有一個實例,減少了記憶體的開銷,尤其是頻繁的創建和銷毀實例(比如管理學院首頁頁面緩存)。
- 2、避免對資源的多重占用(比如寫文件操作)。
缺點:沒有介面,不能繼承,與單一職責原則衝突,一個類應該只關心內部邏輯,而不關心外面怎麼樣來實例化。
使用場景:
- 1、要求生產唯一序列號。
- 2、WEB 中的計數器,不用每次刷新都在資料庫裡加一次,用單例先緩存起來。
- 3、創建的一個對象需要消耗的資源過多,比如 I/O 與資料庫的連接等。
註意事項:getInstance() 方法中需要使用同步鎖防止多線程同時進入造成 instance 被多次實例化。
雙重”鎖定“
原子操作,是併發編程中”最小的且不可並行化“的操作。本案例中使用原子操作配合互斥鎖實現了非常高效的單例模式。互斥鎖的代價比普通整數的原子讀寫高很多,所以在性能敏感的地方添加一個initialized
標誌位,通過原子檢測標誌位狀態降低互斥鎖的次數來提高性能。(也可以使用實例來進行判斷,在實例未被創建的時候再加鎖處理)
在互斥鎖之後還要判斷實例是否存在,是因為當多線程的時候,當一個線程處理完退出解鎖,另一個在排隊等候的線程進入後如果沒有實例的判斷,那麼會再生成一遍實例,沒有達到單例的目的。
靜態初始化
C# 提供了靜態初始化的方法,這種方法可以解決多線程環境下不安全的原因。
C# 給類添加sealed關鍵字防止子類繼承產生多個單例、給靜態欄位添加readonly修改為只讀狀態,意味著只能在靜態初始化期間或在類構造函數中分配變數。
這種靜態初始化的方式是在自己被載入時就將自己實例化,所以被形象地稱之為餓漢式單例類,原先的單例模式處理方式是要在被第一次引用時才會自己實例化,這叫懶漢式初始化。
餓漢式初始化是類一載入就實例化的對象,所以要提前占用系統資源。然而懶漢式,又會面臨多線程訪問的安全性問題,需要做雙重鎖定才能保證安全。
在golang實現靜態初始化,實際上只要把實例化的過程移動到當前文件的init
函數中,在包被載入的過程中,會首先運行各個文件的init
函數,再運行main
函數。
實用類與單例類的比較
在C#經常有工具類之說,這個工具類包含許多靜態方法和靜態屬性。但是這種實用類沒有單例類的狀態。實用類不能用於繼承多台,而單例類雖然實例唯一,但是可以有子類來繼承。實用類是一些方法屬性的集合,單例是有著唯一對象的實例。
參考資料
- 《Go語言核心編程》李文塔
- 《Go語言高級編程》柴樹彬、曹春輝
- 《大話設計模式》程傑
- 單例模式 | 菜鳥教程