我的設計模式之旅、02 單例模式(第二次更新)

来源:https://www.cnblogs.com/linxiaoxu/archive/2022/09/19/16672173.html
-Advertisement-
Play Games

我的設計模式之旅。本節詳細說明單例模式的實現方式、優缺點,簡要描述多線程情況下利用雙重鎖定保護單例對象和C#靜態初始化的方式。並用 Golang 實現單例模式,三個工作者需要各自找到電梯搭乘,只有一個電梯!補充C#單線程單例模式的實現。 ...


編程旅途是漫長遙遠的,在不同時刻有不同的感悟,本文會一直更新下去。

思考總結

什麼是單例模式

單例模式(Singleton Pattern)屬於創建型模式,它提供了一種創建對象的最佳方式。

單例模式:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。

image-20220919005657221

含義:

  • 這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。

  • 職責角度看,實例化與否不應該由使用方判斷,而是應該由自己來判斷。將實例化判斷過程遷移到GetInstance()函數。

  • Singleton類封裝它的唯一實例,這樣它可以嚴格地控制客戶怎樣訪問它以及何時訪問它。簡單地說就是對唯一實例的受控訪問。我們可以更改GetInstance() 函數添加條件和參數實現受控訪問。

註意:

  • 單例類必須自己創建自己的唯一實例。
  • 單例類必須給所有其他對象提供這一實例。
  • 通常情況下,單例類只能有一個實例。
  • 在特殊情況下,你可以隨時調整限制並設定生成單例實例的數量, 只需修改獲取實例方法, 即 getInstance 中的代碼即可實現。

實現方式:

  • 將預設構造函數設為私有, 防止其他對象使用單例類的 new 運算符。

  • 新建一個靜態構建方法作為構造函數。該函數會“偷偷”調用私有構造函數來創建對象,並將其保存在一個靜態成員變數中。此後所有對於該函數的調用都將返回這一緩存對象(判斷系統是否已經有這個單例,如果有則返回,如果沒有則創建)

  • 檢查客戶端代碼,將對單例的構造函數的調用替換為對其靜態構建方法的調用。

  • 需要私有靜態成員變數保存單例、公有靜態構建方法獲取單例、類的構造函數私有化保護單例實例。

應用實例:

  • 1、一個班級只有一個班主任。
  • 2、Windows 是多進程多線程的,在操作一個文件的時候,就不可避免地出現多個進程或線程同時操作一個文件的現象,所以所有文件的處理必須通過唯一的實例來進行。
  • 3、一些設備管理器常常設計為單例模式,比如一個電腦有兩台印表機,在輸出的時候就要處理不能兩台印表機列印同一個文件。

優點:

  • 1、在記憶體里只有一個實例,減少了記憶體的開銷,尤其是頻繁的創建和銷毀實例(比如管理學院首頁頁面緩存)。
  • 2、避免對資源的多重占用(比如寫文件操作)。

缺點:

  • 違反了單一職責原則。該模式同時解決了兩個問題。
  • 單例模式可能掩蓋不良設計,比如程式各組件之間相互瞭解過多等。
  • 該模式在多線程環境下需要進行特殊處理,避免多個線程多次創建單例對象。
    • getInstance() 方法中需要使用同步鎖防止多線程同時進入造成 instance 被多次實例化。
  • 單例的客戶端代碼單元測試可能會比較困難,因為許多測試框架以基於繼承的方式創建模擬對象。由於單例類的構造函數是私有的,而且絕大部分語言無法重寫靜態方法,所以你需要想出仔細考慮模擬單例的方法。要麼乾脆不編寫測試代碼,或者不使用單例模式。

使用場景:

  • 1、要求生產唯一序列號。
  • 2、WEB 中的計數器,不用每次刷新都在資料庫裡加一次,用單例先緩存起來。
  • 3、創建的一個對象需要消耗的資源過多,比如 I/O 與資料庫的連接等。

與其他模式的關係:

  • 外觀類通常可以轉換為單例類,因為在大部分情況下一個外觀對象就足夠了。
  • 如果你能將對象的所有共用狀態簡化為一個享元對象,那麼享元就和單例類似了。但這兩個模式有兩個根本性的不同。
    • 只會有一個單例實體,但是享元類可以有多個實體,各實體的內在狀態也可以不同。
    • 單例對象可以是可變的。享元對象是不可變的。
  • 抽象工廠、生成器和原型都可以用單例來實現。

雙重”鎖定“

原子操作,是併發編程中”最小的且不可並行化“的操作。本案例中使用原子操作配合互斥鎖實現了非常高效的單例模式。互斥鎖的代價比普通整數的原子讀寫高很多,所以在性能敏感的地方添加一個initialized標誌位,通過原子檢測標誌位狀態降低互斥鎖的次數來提高性能。(也可以使用實例來進行判斷,在實例未被創建的時候再加鎖處理)

在互斥鎖之後還要判斷實例是否存在,是因為當多線程的時候,當一個線程處理完退出解鎖,另一個在排隊等候的線程進入後如果沒有實例的判斷,那麼會再生成一遍實例,沒有達到單例的目的。

靜態初始化

C# 提供了靜態初始化的方法,這種方法可以解決多線程環境下不安全的原因。

C# 給類添加sealed關鍵字防止子類繼承產生多個單例、給靜態欄位添加readonly修改為只讀狀態,意味著只能在靜態初始化期間或在類構造函數中分配變數。

這種靜態初始化的方式是在自己被載入時就將自己實例化,所以被形象地稱之為餓漢式單例類,原先的單例模式處理方式是要在被第一次引用時才會自己實例化,這叫懶漢式初始化。

餓漢式初始化是類一載入就實例化的對象,所以要提前占用系統資源。然而懶漢式,又會面臨多線程訪問的安全性問題,需要做雙重鎖定才能保證安全。

在golang實現靜態初始化,實際上只要把實例化的過程移動到當前文件的init函數中,在包被載入的過程中,會首先運行各個文件的init函數,再運行main函數。

實用類與單例類的比較

在C#經常有工具類之說,這個工具類包含許多靜態方法和靜態屬性。但是這種實用類沒有單例類的狀態。實用類不能用於繼承多台,而單例類雖然實例唯一,但是可以有子類來繼承。實用類是一些方法屬性的集合,單例是有著唯一對象的實例。

程式介紹

image-20220908221824615

本程式實現了單例模式,三個工作者需要各自找到電梯搭乘!電梯只有一個!

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, "正在搭乘電梯!")
	}
}

// 由於Golang不支持靜態方法、靜態欄位,所以使用純函數替代面向對象中的靜態方法
// 乘客想知道電梯在哪,單例模式
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
    }
}

所有類都有構造方法,不編碼則系統預設生成空的構造方法,若有顯示定義的構造方法,預設的構造方法就會失效。

參考資料

  • 《Go語言核心編程》李文塔
  • 《Go語言高級編程》柴樹彬、曹春輝
  • 《大話設計模式》程傑
  • 《深入設計模式》亞歷山大·什韋茨
  • 單例模式 | 菜鳥教程

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • MySQL的通用日誌: 用來記錄對資料庫的通用操作,包括錯誤的sql語句等信息。 通用日誌可以保存在:file(預設值)或 table(mysql.general_log表) mysql通用日誌的設置: general_log=ON|OFF 是否啟用通用日誌 general_log_file=HOS ...
  • 1 - 編程語言 1.1 編程 編程: 就是讓電腦為解決某個問題而使用某種程式設計語言編寫程式代碼,並最終得到結果的過程。 電腦程式: 就是電腦所執行的一系列的指令集合,而程式全部都是用我們所掌握的語言來編寫的,所以人們要控制電腦一定要通過電腦語言向電腦發出命令。 1.2 電腦語言 計 ...
  • 移動web開發之rem佈局 rem基礎 rem單位 rem (root em)是一個相對單位,類似於em,em是父元素字體大小。 不同的是rem的基準是相對於html元素的字體大小。 比如,根元素(html)設置font-size=12px; 非根元素設置width:2rem; 則換成px表示就是2 ...
  • 這個系列的目的是通過使用 JS 實現“乞丐版”的 React,讓讀者瞭解 React 的基本工作原理,體會 React 帶來的構建應用的優勢 1 HTML 構建靜態頁面 使用 HTML 和 CSS,我們很容易可以構建出上圖中的頁面 <!DOCTYPE html> <html lang="en"> < ...
  • 原型(prototype)是函數特有的屬性。只要創建了一個函數,這個函數就會自動創建一個prototype屬性(顯式原型),並指向該函數的原型對象。原型對象上都有一個constructor屬性,指向prototype屬性所在的函數(即函數本身)。而對於每一個構造函數創建出的實例對象,內部都會有一個[... ...
  • 移動端中的元素內容超出時,對容器設置overflow-x: auto就可以通過手勢水平移動。但是 PC 端只能通過滑鼠滾輪上下滑動,而不能水平移動。 只需要給元素添加一個監聽滑鼠滾輪事件,上下滑動時修改其 scrollLeft 屬性值就可以實現。直接貼上代碼: <div class="horizon ...
  • 我的設計模式之旅,本節學習原型模式。從複製原有對象出現的兩大問題思考原型模式存在的必要性。探討原型模式的實現方法。 ...
  • 在寫開源項目的時候,想到了要支持多種redis部署方式,於是對於這塊的生產環境的架構選型展開調研。 推薦使用更新的引擎版本以支持更多的特性, Redis 6.0新特性說明 模塊系統新增多個API。 支持SSL/TLS加密。 支持新的Redis協議:RESP3。 服務端支持多模式的客... ...
一周排行
    -Advertisement-
    Play Games
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • 目錄前言PostgreSql安裝測試額外Nuget安裝Person.cs模擬運行Navicate連postgresql解決方案Garnet為什麼要選擇Garnet而不是RedisRedis不再開源Windows版的Redis是由微軟維護的Windows Redis版本老舊,後續可能不再更新Garne ...
  • C#TMS系統代碼-聯表報表學習 領導被裁了之後很快就有人上任了,幾乎是無縫銜接,很難讓我不想到這早就決定好了。我的職責沒有任何變化。感受下來這個系統封裝程度很高,我只要會調用方法就行。這個系統交付之後不會有太多問題,更多應該是做小需求,有大的開發任務應該也是第二期的事,嗯?怎麼感覺我變成運維了?而 ...
  • 我在隨筆《EAV模型(實體-屬性-值)的設計和低代碼的處理方案(1)》中介紹了一些基本的EAV模型設計知識和基於Winform場景下低代碼(或者說無代碼)的一些實現思路,在本篇隨筆中,我們來分析一下這種針對通用業務,且只需定義就能構建業務模塊存儲和界面的解決方案,其中的數據查詢處理的操作。 ...
  • 對某個遠程伺服器啟用和設置NTP服務(Windows系統) 打開註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer 將 Enabled 的值設置為 1,這將啟用NTP伺服器功 ...
  • title: Django信號與擴展:深入理解與實踐 date: 2024/5/15 22:40:52 updated: 2024/5/15 22:40:52 categories: 後端開發 tags: Django 信號 松耦合 觀察者 擴展 安全 性能 第一部分:Django信號基礎 Djan ...
  • 使用xadmin2遇到的問題&解決 環境配置: 使用的模塊版本: 關聯的包 Django 3.2.15 mysqlclient 2.2.4 xadmin 2.0.1 django-crispy-forms >= 1.6.0 django-import-export >= 0.5.1 django-r ...
  • 今天我打算整點兒不一樣的內容,通過之前學習的TransformerMap和LazyMap鏈,想搞點不一樣的,所以我關註了另外一條鏈DefaultedMap鏈,主要調用鏈為: 調用鏈詳細描述: ObjectInputStream.readObject() DefaultedMap.readObject ...
  • 後端應用級開發者該如何擁抱 AI GC?就是在這樣的一個大的浪潮下,我們的傳統的應用級開發者。我們該如何選擇職業或者是如何去快速轉型,跟上這樣的一個行業的一個浪潮? 0 AI金字塔模型 越往上它的整個難度就是職業機會也好,或者說是整個的這個運作也好,它的難度會越大,然後越往下機會就會越多,所以這是一 ...
  • @Autowired是Spring框架提供的註解,@Resource是Java EE 5規範提供的註解。 @Autowired預設按照類型自動裝配,而@Resource預設按照名稱自動裝配。 @Autowired支持@Qualifier註解來指定裝配哪一個具有相同類型的bean,而@Resourc... ...