使用sync.Once實現高效的單例模式

来源:https://www.cnblogs.com/chenjiazhan/archive/2023/03/16/17223968.html
-Advertisement-
Play Games

一、前期準備 1、首先需要安裝並配置好本地JDK(WIN+R輸入cmd,輸入java -version如下圖) 2、下載maven到本地(鏈接Maven – Download Apache Maven) 其他歷史版本在這裡找:Index of /maven/maven-3 (apache.org) ...


1. 簡介

本文介紹使用sync.Once來實現單例模式,包括單例模式的定義,以及使用sync.Once實現單例模式的示例,同時也比較了其他單例模式的實現。最後以一個開源框架中使用sync.Once實現單例模式的例子來作為結尾。

2. 基本實現

2.1 單例模式定義

單例模式是一種創建型設計模式,它保證一個類只有一個實例,並提供一個全局訪問點來訪問這個實例。在整個應用程式中,所有對於這個類的訪問都將返回同一個實例對象。

2.2 sync.Once實現單例模式

下麵是一個簡單的示例代碼,使用 sync.Once 實現單例模式:

 package singleton

 import "sync"

 type singleton struct {
     // 單例對象的狀態
 }

 var (
     instance *singleton
     once     sync.Once
 )

 func GetInstance() *singleton {
     once.Do(func() {
         instance = &singleton{}
         // 初始化單例對象的狀態
     })
     return instance
 }

在上面的示例代碼中,我們定義了一個 singleton 結構體表示單例對象的狀態,然後將它的實例作為一個包級別的變數 instance,並使用一個 once 變數來保證 GetInstance 函數只被執行一次。

GetInstance 函數中,我們使用 once.Do 方法來執行一個初始化單例對象。由於 once.Do 方法是基於原子操作實現的,因此可以保證併發安全,即使有多個協程同時調用 GetInstance 函數,最終也只會創建一個對象。

2.3 其他方式實現單例模式

2.3.1 全局變數定義時賦值,實現單例模式

在 Go 語言中,全局變數會在程式啟動時自動初始化。因此,如果在定義全局變數時給它賦值,則對象的創建也會在程式啟動時完成,可以通過此來實現單例模式,以下是一個示例代碼:

type MySingleton struct {
    // 欄位定義
}

var mySingletonInstance = &MySingleton{
    // 初始化欄位
}

func GetMySingletonInstance() *MySingleton {
    return mySingletonInstance
}

在上面的代碼中,我們定義了一個全局變數 mySingletonInstance 併在定義時進行了賦值,從而在程式啟動時完成了對象的創建和初始化。在 GetMySingletonInstance 函數中,我們可以直接返回全局變數 mySingletonInstance,從而實現單例模式。

2.3.2 init 函數實現單例模式

在 Go 語言中,我們可以使用 init 函數來實現單例模式。init 函數是在包被載入時自動執行的函數,因此我們可以在其中創建並初始化單例對象,從而保證在程式啟動時就完成對象的創建。以下是一個示例代碼:

package main

type MySingleton struct {
    // 欄位定義
}

var mySingletonInstance *MySingleton

func init() {
    mySingletonInstance = &MySingleton{
        // 初始化欄位
    }
}

func GetMySingletonInstance() *MySingleton {
    return mySingletonInstance
}

在上面的代碼中,我們定義了一個包級別的全局變數 mySingletonInstance,併在 init 函數中創建並初始化了該對象。在 GetMySingletonInstance 函數中,我們直接返回該全局變數,從而實現單例模式。

2.3.3 使用互斥鎖實現單例模式

在 Go 語言中,可以只使用一個互斥鎖來實現單例模式。下麵是一個簡單代碼的演示:

var instance *MySingleton
var mu sync.Mutex

func GetMySingletonInstance() *MySingleton {
   mu.Lock()
   defer mu.Unlock()

   if instance == nil {
      instance = &MySingleton{
         // 初始化欄位
      }
   }
   return instance
}

在上面的代碼中,我們使用了一個全局變數instance來存儲單例對象,並使用了一個互斥鎖 mu 來保證對象的創建和初始化。具體地,我們在 GetMySingletonInstance 函數中首先加鎖,然後判斷 instance 是否已經被創建,如果未被創建,則創建並初始化對象。最後,我們釋放鎖並返回單例對象。

需要註意的是,在併發高的情況下,使用一個互斥鎖來實現單例模式可能會導致性能問題。因為在一個 goroutine 獲得鎖並創建對象時,其他的 goroutine 都需要等待,這可能會導致程式變慢。

2.4 使用sync.Once實現單例模式的優點

相對於init 方法和使用全局變數定義賦值單例模式的實現,sync.Once 實現單例模式可以實現延遲初始化,即在第一次使用單例對象時才進行創建和初始化。這可以避免在程式啟動時就進行對象的創建和初始化,以及可能造成的資源的浪費。

而相對於使用互斥鎖實現單例模式,使用 sync.Once 實現單例模式的優點在於更為簡單和高效。sync.Once提供了一個簡單的介面,只需要傳遞一個初始化函數即可。相比互斥鎖實現方式需要手動處理鎖、判斷等操作,使用起來更加方便。而且使用互斥鎖實現單例模式需要在每次訪問單例對象時進行加鎖和解鎖操作,這會增加額外的開銷。而使用 sync.Once 實現單例模式則可以避免這些開銷,只需要在第一次訪問單例對象時進行一次初始化操作即可。

但是也不是說sync.Once便適合所有的場景,這個是需要具體情況具體分析的。下麵說明sync.Onceinit方法,在哪些場景下使用init更好,在哪些場景下使用sync.Once更好。

2.5 sync.Once和init方法適用場景

對於init實現單例,比較適用於在程式啟動時就需要初始化變數的場景。因為init函數是在程式運行前執行的,可以確保變數在程式運行時已經被初始化。

對於需要延遲初始化某些對象,對象被創建出來並不會被馬上使用,或者可能用不到,例如創建資料庫連接池等。這時候使用sync.Once就非常合適。它可以保證對象只被初始化一次,並且在需要使用時才會被創建,避免不必要的資源浪費。

3. gin中單例模式的使用

3.1 背景

這裡首先需要介紹下gin.Engine, gin.Engine是Gin框架的核心組件,負責處理HTTP請求,路由請求到對應的處理器,處理器可以是中間件、控制器或處理HTTP響應等。每個gin.Engine實例都擁有自己的路由表、中間件棧和其他配置項,通過調用其方法可以註冊路由、中間件、處理函數等。

一個HTTP伺服器,只會存在一個對應的gin.Engine實例,其保存了路由映射規則等內容。

為了簡化開發者Gin框架的使用,不需要用戶創建gin.Engine實例,便能夠完成路由的註冊等操作,提高代碼的可讀性和可維護性,避免重覆代碼的出現。這裡對於一些常用的功能,抽取出一些函數來使用,函數簽名如下:

// ginS/gins.go
// 載入HTML模版文件
func LoadHTMLGlob(pattern string) {}
// 註冊POST請求處理器
func POST(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {}
// 註冊GET請求處理器
func GET(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {}
// 啟動一個HTTP伺服器
func Run(addr ...string) (err error) {}
// 等等...

接下來需要對這些函數來進行實現。

3.2 具體實現

首先從使用出發,這裡使用POST方法/GET方法註冊請求處理器,然後使用Run方法啟動伺服器:

func main() {
   // 註冊url對應的處理器
   POST("/login", func(c *gin.Context) {})
   // 註冊url對應的處理器
   GET("/hello", func(c *gin.Context) {})
   // 啟動服務
   Run(":8080")
}

這裡我們想要的效果,應該是調用Run方法啟動服務後,往/login路徑發送請求,此時應該執行我們註冊的對應處理器,往/hello路徑發送請求也是同理。

所以,這裡POST方法,GET方法,Run方法應該都是對同一個gin.Engine 進行操作的,而不是各自使用各自的gin.Engine實例,亦或者每次調用就創建一個gin.Engine實例。這樣子才能達到我們預想的效果。

所以,我們需要實現一個方法,獲取gin.Engine實例,每次調用該方法都是獲取到同一個實例,這個其實也就是單例的定義。然後POST方法,GET方法又或者是Run方法,調用該方法獲取到gin.Engine實例,然後調用實例去調用對應的方法,完成url處理器的註冊或者是服務的啟動。這樣子就能夠保證是使用同一個gin.Engine實例了。具體實現如下:

// ginS/gins.go
import (
   "github.com/gin-gonic/gin"
)
var once sync.Once
var internalEngine *gin.Engine

func engine() *gin.Engine {
   once.Do(func() {
      internalEngine = gin.Default()
   })
   return internalEngine
}
// POST is a shortcut for router.Handle("POST", path, handle)
func POST(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
   return engine().POST(relativePath, handlers...)
}

// GET is a shortcut for router.Handle("GET", path, handle)
func GET(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
   return engine().GET(relativePath, handlers...)
}

這裡engine() 方法使用了 sync.Once 實現單例模式,確保每次調用該方法返回的都是同一個 gin.Engine 實例。然後POST/GET/Run方法通過該方法獲取到gin.Engine實例,然後調用實例中對應的方法來完成對應的功能,從而達到POST/GET/Run等方法都是使用同一個實例操作的效果。

3.3 sync.Once實現單例的好處

這裡想要達到的目的,其實是GET/POST/Run等抽取出來的函數,使用同一個gin.Engine實例。

為了達到這個目的,我們其實可以在定義internalEngine 變數時,便對其進行賦值;或者是通init函數完成對internalEngine變數的賦值,其實都可以。

但是我們抽取出來的函數,用戶並不一定使用,定義時便初始化或者在init方法中便完成了對變數的賦值,用戶沒使用的話,創建出來的gin.Engine實例沒有實際用途,造成了不必要的資源的浪費。

而engine方法使用sync.Once實現了internalEngin的延遲初始化,只有在真正使用到internalEngine時,才會對其進行初始化,避免了不必要的資源的浪費。

這裡其實也印證了上面我們所說的sync.Once的適用場景,對於不會馬上使用的單例對象,此時可以使用sync.Once來實現。

4.總結

單例模式是一種常用的設計模式,用於保證一個類僅有一個實例。在單例模式中,常常使用互斥鎖或者變數賦值的方式來實現單例。然而,使用sync.Once可以更方便地實現單例,同時也能夠避免了不必要的資源浪費。當然,沒有任何一種實現是適合所有場景的,我們需要根據具體場景具體分析。


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

-Advertisement-
Play Games
更多相關文章
  • 這篇文章主要聊一下緩存,如何使用緩存來加速你的系統,減少磁碟 IO。按照讀寫性質,緩存可以分為讀寫緩存和只讀緩存,兩種緩存有各自的適用場景。 ...
  • 這篇文章主要用來討論Kafka是如何做到高性能的,包括使用批處理方式處理消息,使用順序讀寫的方式使用磁碟,利用PageCache緩存數據並減少IO操作,使用零拷貝技術加速消費流程。 ...
  • 三維模型幾何糾正方法主要包括以下幾種:坐標變換法:通過對三維模型的坐標進行變換,實現幾何糾正。常用的坐標變換包括平移、旋轉和縮放等。平移和旋轉可以通過對模型的平移和旋轉矩陣進行計算實現,縮放可以通過對模型的坐標進行縮放繫數的計算實現。點雲擬合法:將三維模型擬合到點雲數據上,通過對擬合誤差進行優化,實 ...
  • 1. G1垃圾回收器 1.1. 垃圾優先(garbage first) 1.2. 在堆內離散的區域上進行操作 1.2.1. 預設大約有2048個 1.2.2. 代的區域不需要是連續的 1.2.3. 可能屬於老年代 1.2.3.1. 併發後臺線程尋找沒有被引用的對象時,一些區域會比其他區域有更多的垃圾 ...
  • 01_GoLand debug時出現Connected並且程式卡住的問題 環境:win10、go version go1.19.4 windows/amd64、GoLand 2020.3.5 x64 現象 : 在 debug 模式下運行項目,打上斷點後,可以進入斷點位置,也可以跳轉到下個斷點,但是, ...
  • 編寫程式過程中,我們有時不希望改變某個變數的值。此時就可以使用關鍵字 const 對變數的類型加以限定。 初始化和const 因為const對象一旦創建後其值就不能再改變,所以const對象必須初始化。一如既往,初始值可以是任意複雜的表達式: const int i = get_size();//正 ...
  • yaml 1.yaml介紹 YAML是 "YAML Ain't a Markup Language" (YAML不是一種標記語言)的遞歸縮寫。在開發這種語言時,YAML的意思其實是:"Yet Another Markup Language"(仍是一種標記語言),是為了強調這種語言以數據為中心,而不是 ...
  • Lombok、Spring-Initializer 1.Lombok 1.1Lombok介紹 Lombok的作用是: 簡化Javabean的開發,可以使用Lombok的註解讓代碼更加簡潔 Java項目中,很多沒有技術含量又必須存在的代碼:比如POJO類的getter、setter、toString方 ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...