天天看點

不同語言對單例模式的不同實作

不同語言對單例模式的不同實作

前段時間在用 <code>Python</code> 實作業務的時候發現一個坑,準确的來說是對于 <code>Python</code> 門外漢容易踩的坑;

大概代碼如下:

首先定義了一個 <code>Mom</code> 的類,它包含了一個字元串類型的 <code>name</code> 與清單類型的 <code>sons</code> 屬性;

在使用時首先建立了該類的一個執行個體 <code>m1</code> 并往 <code>sons</code> 中寫入一個清單資料;緊接着又建立了一個執行個體 <code>m2</code> ,也往 <code>sons</code> 中寫入了另一個清單資料。

如果是一個 <code>Javaer</code> 很少寫 <code>Python</code> 看到這樣的代碼首先想到的輸出應該是:

但其實最終的輸出結果是:

如果想要達到期望值需要稍微修改一下:

隻需要修改類的定義就可以了,我相信即使沒有 <code>Python</code> 相關經驗對比這兩個代碼應該也能猜到原因:

在 <code>Python</code> 中如果需要将變量作為執行個體變量(也就是每個我們期望的輸出)時,需要将變量定義到構造函數中,通過 <code>self</code> 通路。

如果隻放在類中,和 <code>Java</code> 中的 <code>static</code> 靜态變量效果類似;這些資料由類共享,也就能解釋為什麼會出現第一種情況,因為其中的 <code>sons</code> 是由 <code>Mom</code> 類共享,是以每次都會累加。

既然 <code>Python</code> 可以通過類變量達到變量在同一個類中共享的效果,那是否可以實作單例模式呢?

可以利用 <code>Python</code> 的 <code>metaclass</code> 的特性,動态的控制類的建立。

首先建立一個 <code>Singleton</code> 的基類,然後我們在我們需要實作單例的類中将其作為 <code>metaclass</code>

這樣<code>Singleton</code> 就可以控制 <code>MySQLDriver</code> 這個類的建立了;其實在 <code>Singleton</code> 中的 <code>__call__</code> 可以很容易了解這個單例建立的過程:

定義一個私有的類屬性 <code>_instances</code> 的字典(也就是 <code>Java</code> 中的 <code>map</code>)可以做到在整個類中共享,無論建立多少個執行個體。

當我們自定義類使用了 <code>__metaclass__ = Singleton</code> 後,便可以控制自定義類的建立了;如果已經建立了執行個體,那就直接從 <code>_instances</code> 取出對象傳回,不然就建立一個執行個體并寫回到 <code>_instances</code> ,有點 <code>Spring</code> 容器的感覺。

最後我們通過實驗結果可以看到單例建立成功。

由于最近團隊中有部分業務開始在用 <code>go</code> ,是以也想看看在 <code>go</code> 中如何實作單例。

在這樣一個簡單的結構體(可以簡單了解為 <code>Java</code> 中的 <code>class</code>)中是沒法類似于 <code>Python</code> 和 <code>Java</code> 一樣可以聲明類共享變量的;<code>go</code> 語言中不存在 <code>static</code> 的概念。

但我們可以在包中聲明一個全局變量來達到同樣的效果:

這樣在使用時:

就不需要直接構造 <code>MySQLDriver</code> ,而是通過<code>GetDriver()</code> 函數來擷取,通過 <code>debug</code> 也能看到 <code>driver</code> 和 <code>driver1</code> 引用的是同一個記憶體位址。

不同語言對單例模式的不同實作

這樣的實作正常情況是沒有什麼問題的,機智的朋友一定能想到和 <code>Java</code> 一樣,一旦并發通路就沒那麼簡單了。

在 <code>go</code> 中,如果有多個 <code>goroutine</code> 同時通路<code>GetDriver()</code> ,那大機率會建立多個 <code>MySQLDriver</code> 執行個體。

這裡說的沒那麼簡單其實是相對于 <code>Java</code> 來說的,<code>go</code> 語言中提供了簡單的 <code>api</code> 便可實作臨界資源的通路。

稍加改造上文的代碼,加入了

代碼就能簡單的控制臨界資源的通路,即便我們開啟了100個協程并發執行,<code>mySQLDriver</code> 執行個體也隻會被初始化一次。

這裡的 <code>defer</code> 類似于 <code>Java</code> 中的 <code>finally</code> ,在方法調用前加上 <code>go</code> 關鍵字即可開啟一個協程。

雖說能滿足并發要求了,但其實這樣的實作也不夠優雅;仔細想想這裡

建立執行個體隻會調用一次,但後續的每次調用都需要加鎖進而帶來了不必要的開銷。

這樣的場景每個語言都是相同的,拿 <code>Java</code> 來說是不是經常看到這樣的單例實作:

這是一個典型的雙重檢查的單例,這裡做了兩次檢查便可以避免後續其他線程再次通路鎖。

同樣的對于 <code>go</code> 來說也類似:

和 <code>Java</code> 一樣,在原有基礎上額外做一次判斷也能達到同樣的效果。

但有沒有覺得這樣的代碼非常繁瑣,這一點 <code>go</code> 提供的 <code>api</code> 就非常省事了:

本質上我們隻需要不管在什麼情況下 <code>MySQLDriver</code> 執行個體隻初始化一次就能達到單例的目的,是以利用 <code>once.Do()</code> 就能讓代碼隻執行一次。

不同語言對單例模式的不同實作

檢視源碼會發現 <code>once.Do()</code> 也是通過鎖來實作,隻是在加鎖之前利用底層的原子操作做了一次校驗,進而避免每次都要加鎖,性能會更好。

相信大家日常開發中很少會碰到需要自己實作一個單例;首先大部分情況下我們都不需要單例,即使是需要,架構通常也都有內建。

類似于 <code>go</code> 這樣架構較少,需要我們自己實作時其實也不需要過多考慮并發的問題;摸摸自己肚子左上方的位置想想,自己寫的這個對象真的同時有幾百上千的并發來建立嘛?

不過通過這個對比會發現 <code>go</code> 的文法确實要比 <code>Java</code> 簡潔太多,同時輕量級的協程以及簡單易用的并發工具支援看起來都要比 <code>Java</code> 優雅許多;後續有機會再接着深入。

參考連結:

Creating a singleton in Python

How to implement Singleton Pattern in Go

作者:

crossoverJie

出處:

https://crossoverjie.top

不同語言對單例模式的不同實作

歡迎關注部落客公衆号與我交流。

本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出,

如有問題, 可郵件(crossoverJie#gmail.com)咨詢。