不一樣語言對單例模式的不一樣實現

前言

前段時間在用 Python 實現業務的時候發現一個坑,準確的來講是對於 Python 門外漢容易踩的坑;java

大概代碼以下:python

class Mom(object):
    name = ''
    sons = []

if __name__ == '__main__':
    m1 = Mom()
    m1.name = 'm1'
    m1.sons.append(['s1', 's2'])
    print '{} sons={}'.format(m1.name, m1.sons)

    m2 = Mom()
    m2.name = 'm2'
    m2.sons.append(['s3', 's4'])
    print '{} sons={}'.format(m2.name, m2.sons)
複製代碼

首先定義了一個 Mom 的類,它包含了一個字符串類型的 name 與列表類型的 sons 屬性;golang

在使用時首先建立了該類的一個實例 m1 並往 sons 中寫入一個列表數據;緊接着又建立了一個實例 m2 ,也往 sons 中寫入了另外一個列表數據。api

若是是一個 Javaer 不多寫 Python 看到這樣的代碼首先想到的輸出應該是:markdown

m1 sons=[['s1', 's2']]
m2 sons=[['s3', 's4']]
複製代碼

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

m1 sons=[['s1', 's2']]
m2 sons=[['s1', 's2'], ['s3', 's4']]
複製代碼

若是想要達到指望值須要稍微修改一下:app

class Mom(object):
    name = ''

    def __init__(self):
        self.sons = []
複製代碼

只須要修改類的定義就能夠了,我相信即便沒有 Python 相關經驗對比這兩個代碼應該也能猜到緣由:框架

Python 中若是須要將變量做爲實例變量(也就是每一個咱們指望的輸出)時,須要將變量定義到構造函數中,經過 self 訪問。函數

若是隻放在類中,和 Java 中的 static 靜態變量效果相似;這些數據由類共享,也就能解釋爲何會出現第一種狀況,由於其中的 sons 是由 Mom 類共享,因此每次都會累加。工具

Python 單例

既然 Python 能夠經過類變量達到變量在同一個類中共享的效果,那是否能夠實現單例模式呢?

能夠利用 Pythonmetaclass 的特性,動態的控制類的建立。

class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]
複製代碼

首先建立一個 Singleton 的基類,而後咱們在咱們須要實現單例的類中將其做爲 metaclass

class MySQLDriver:
    __metaclass__ = Singleton

    def __init__(self):
        print 'MySQLDriver init.....'
複製代碼

這樣Singleton 就能夠控制 MySQLDriver 這個類的建立了;其實在 Singleton 中的 __call__ 能夠很容易理解這個單例建立的過程:

  • 定義一個私有的類屬性 _instances 的字典(也就是 Java 中的 map)能夠作到在整個類中共享,不管建立多少個實例。
  • 當咱們自定義類使用了 __metaclass__ = Singleton 後,即可以控制自定義類的建立了;若是已經建立了實例,那就直接從 _instances 取出對象返回,否則就建立一個實例並寫回到 _instances ,有點 Spring 容器的感受。
if __name__ == '__main__':
    m1 = MySQLDriver()
    m2 = MySQLDriver()
    m3 = MySQLDriver()
    m4 = MySQLDriver()
    print m1
    print m2
    print m3
    print m4

MySQLDriver init.....
<__main__.MySQLDriver object at 0x10d848790>
<__main__.MySQLDriver object at 0x10d848790>
<__main__.MySQLDriver object at 0x10d848790>
<__main__.MySQLDriver object at 0x10d848790>
複製代碼

最後咱們經過實驗結果能夠看到單例建立成功。

Go 單例

因爲最近團隊中有部分業務開始在用 go ,因此也想看看在 go 中如何實現單例。

type MySQLDriver struct {
	username string
}
複製代碼

在這樣一個簡單的結構體(能夠簡單理解爲 Java 中的 class)中是無法相似於 PythonJava 同樣能夠聲明類共享變量的;go 語言中不存在 static 的概念。

但咱們能夠在包中聲明一個全局變量來達到一樣的效果:

import "fmt"

type MySQLDriver struct {
	username string
}

var mySQLDriver *MySQLDriver

func GetDriver() *MySQLDriver {
	if mySQLDriver == nil {
		mySQLDriver = &MySQLDriver{}
	}
	return mySQLDriver
}
複製代碼

這樣在使用時:

func main() {
	driver := GetDriver()
	driver.username = "cj"
	fmt.Println(driver.username)

	driver2 := GetDriver()
	fmt.Println(driver2.username)

}
複製代碼

就不須要直接構造 MySQLDriver ,而是經過GetDriver() 函數來獲取,經過 debug 也能看到 driverdriver1 引用的是同一個內存地址。

這樣的實現常規狀況是沒有什麼問題的,機智的朋友必定能想到和 Java 同樣,一旦併發訪問就沒那麼簡單了。

go 中,若是有多個 goroutine 同時訪問GetDriver() ,那大機率會建立多個 MySQLDriver 實例。

這裏說的沒那麼簡單實際上是相對於 Java 來講的,go 語言中提供了簡單的 api 即可實現臨界資源的訪問。

var lock sync.Mutex

func GetDriver() *MySQLDriver {
	lock.Lock()
	defer lock.Unlock()
	if mySQLDriver == nil {
		fmt.Println("create instance......")
		mySQLDriver = &MySQLDriver{}
	}
	return mySQLDriver
}

func main() {
	for i := 0; i < 100; i++ {
		go GetDriver()
	}

	time.Sleep(2000 * time.Millisecond)
}
複製代碼

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

lock.Lock()
defer lock.Unlock()
複製代碼

代碼就能簡單的控制臨界資源的訪問,即使咱們開啓了100個協程併發執行,mySQLDriver 實例也只會被初始化一次。

  • 這裏的 defer 相似於 Java 中的 finally ,在方法調用前加上 go 關鍵字便可開啓一個協程。

雖然說能知足併發要求了,但其實這樣的實現也不夠優雅;仔細想一想這裏

mySQLDriver = &MySQLDriver{}
複製代碼

建立實例只會調用一次,但後續的每次調用都須要加鎖從而帶來了沒必要要的開銷。

這樣的場景每一個語言都是相同的,拿 Java 來講是否是常常看到這樣的單例實現:

public class Singleton {
    private Singleton() {}
   private volatile static Singleton instance = null;
   public static Singleton getInstance() {
        if (instance == null) {     
         synchronized (Singleton.class){
           if (instance == null) {    
             instance = new Singleton();
               }
            }
         }
        return instance;
    }
}
複製代碼

這是一個典型的雙重檢查的單例,這裏作了兩次檢查即可以免後續其餘線程再次訪問鎖。

一樣的對於 go 來講也相似:

func GetDriver() *MySQLDriver {
	if mySQLDriver == nil {
		lock.Lock()
		defer lock.Unlock()
		if mySQLDriver == nil {
			fmt.Println("create instance......")
			mySQLDriver = &MySQLDriver{}
		}
	}
	return mySQLDriver
}
複製代碼

Java 同樣,在原有基礎上額外作一次判斷也能達到一樣的效果。

但有沒有以爲這樣的代碼很是繁瑣,這一點 go 提供的 api 就很是省事了:

var once sync.Once

func GetDriver() *MySQLDriver {
	once.Do(func() {
		if mySQLDriver == nil {
			fmt.Println("create instance......")
			mySQLDriver = &MySQLDriver{}
		}
	})
	return mySQLDriver
}
複製代碼

本質上咱們只須要無論在什麼狀況下 MySQLDriver 實例只初始化一次就能達到單例的目的,因此利用 once.Do() 就能讓代碼只執行一次。

查看源碼會發現 once.Do() 也是經過鎖來實現,只是在加鎖以前利用底層的原子操做作了一次校驗,從而避免每次都要加鎖,性能會更好。

總結

相信你們平常開發中不多會碰到須要本身實現一個單例;首先大部分狀況下咱們都不須要單例,即便是須要,框架一般也都有集成。

相似於 go 這樣框架較少,須要咱們本身實現時其實也不須要過多考慮併發的問題;摸摸本身肚子左上方的位置想一想,本身寫的這個對象真的同時有幾百上千的併發來建立嘛?

不過經過這個對比會發現 go 的語法確實要比 Java 簡潔太多,同時輕量級的協程以及簡單易用的併發工具支持看起來都要比 Java 優雅許多;後續有機會再接着深刻。

參考連接:

Creating a singleton in Python

How to implement Singleton Pattern in Go

相關文章
相關標籤/搜索