golang设计模式--单例模式

119 阅读3分钟

1.单例模式版本 f1

单例模式就是:所有的协程共用一个实例化对象

type DBClient struct {
   Host string
   Port int
}

var dbClient *DBClient

func f1() *DBClient {
   //如果说,连接对象是空的,那么就需要实例化一下
   if dbClient == nil {
      dbClient = &DBClient{}
   }
   return dbClient
}
  • 缺点:如果是多个并发的协程,那么就会出现同一个对象被实例化多遍

  • eg : 协程1拿到【&DBClient】后检查发现为nil,所以就进行实例化操作【dbClient = &DBClient{}】,但是就是在这个过程刚做完,还没有进行return语句退出之前

  • 接着由于高并发的特性,现在需要执行协程2,拿到【&DBClient】后检查发现为nil,所以就进行实例化操作【dbClient = &DBClient{}】,接着又是return语句退出之前

  • 同理,就这样被实例化无数次,最后哪一个协程先返回 dbClient ,dbClient 才不为空

  • 针对这个问题,所以就是要让实例化操作 + return语句处于一个原子操作中

  • 所以:加锁

2.单例模式版本 f2

var lock sync.Mutex
type DBClient struct {
   Host string
   Port int
}

var dbClient *DBClient
func f2() *DBClient {
   lock.Lock()
   defer lock.Unlock() //defer执行时机: return语句 defer解锁 退出
   if dbClient == nil {
      dbClient = &DBClient{}
   }
   return dbClient
}
  • 缺点:损失了高并发的性能
  • 这种操作仅仅适用于少量的并发操作
  • 一进来就锁住代码,这样尽早的让代码逻辑串行化,是十分损耗性能的
  • 所以考虑把上锁拿到判空操作里面

3.单例模式版本 f3

var lock sync.Mutex

type DBClient struct {
   Host string
   Port int
}

var dbClient *DBClient

func f3() *DBClient {
   if dbClient == nil {
      //多个协程并发进行判空操作,比提前锁住串行化的性能损失小一些
      lock.Lock()
      defer lock.Unlock() //defer执行时机: return语句 defer解锁 退出
      dbClient = &DBClient{}
   }
   return dbClient
}
  • 貌似没有问题了,但这样写是存在bug的
  • 锁只是锁住了实例化的过程,但是dbClient的判空是没有锁的
  • 假设:协程1上锁,然后进行实例化,重点来了,细分实例化的步骤,就会出现
  • a.拿到DBClient结构体,b.取地址写入dbClient变量,c.给结构体的字段挨个赋值上零值
  • 可以试想一下,在c步骤中,假如赋值了一半字段后,这时由于高并发的特性协程2开始执行,而每一个协程又都是可以抵达dbClient的判空操作的,接着,协程2就一定会得出dbClient不为空,从而就把这个只赋值了一半零值的返回了
  • 所以,到这里就需要解决一下这个允许其他协程抵达判空操作的问题
  • 常见的方式,就是让其他协程判断另外一个值来得知dbClient为不为空

4.单例模式版本 f4

var lock sync.Mutex
type DBClient struct {
   Host string
   Port int
}
var dbClient *DBClient
var initialed bool = false
func f4() *DBClient {
   if !initialed {
      //多个协程虽然会并发进行判空操作,但是比提前锁住逻辑而串行化的性能损失小一些
      lock.Lock()
      defer lock.Unlock() 
      dbClient = &DBClient{}
      initialed = true
   }
   return dbClient
}
  • 缺点:这样是躲过赋值一半零值就返回的问题
  • 但是新变量【initialed = true】过程,加上判断initialed是不是false,不还是会有这个问题吗
  • 所以:换一个值判断还不够,还需要进行原子性赋值的保证才行

5.单例模式版本 f5

var lock sync.Mutex

type DBClient struct {
   Host string
   Port int
}

var dbClient *DBClient
var NewInitialed int32 = 0

// 由于原子性赋值操作里面是没有atomic.Loadbool的,所以只能使用int32代替
func f5() *DBClient {
   if atomic.LoadInt32(&NewInitialed) == 0 {
      lock.Lock()
      defer lock.Unlock()
      dbClient = &DBClient{}
      atomic.StoreInt32(&NewInitialed, 1)
   }
   if atomic.LoadInt32(&NewInitialed) == 1 {
      return dbClient
   }
   return dbClient
}

截至目前是满足了要求但其实golang自带了一个sync.Once的包,底层就是单例模式代码

6.单例模式版本--最终版

package main

import (
   "sync"
)

type DBClient struct {
   Host string
   Port int
}

var dbClient *DBClient
var once sync.Once

func f6() *DBClient {
   once.Do(func() {
      dbClient = &DBClient{}
   })
   return dbClient
}