设计模式-单例模式

1. 为什么要学习单例模式

单例模式,就是在整个进程的过程中,只会存在一个实例。这样做的好处主要是两个:

  1. 访问控制:访问控制也就是只有这一个口子可以得到该实例,一般是通过全局变量达到这个目的,然后加以访问控制。但是对于go语言来说,访问控制较为简陋,所以一般通过func来将实例暴露在外。
  2. 节约资源:全进程只会初始化一次该实例,甚至可以不初始化,从而节省资源。
    常见的单例场景有:日志系统、连接池等等。也很好理解,一个业务系统的日志系统和连接池一般就是一个,而且需要进程一开始就初始化或者需要的时候初始化一次就可以。

2. 单例模式的类型

为了节省资源,有时候会对单例模式进行延迟初始化,因此根据初始化的时间可以分为:饿汉式单例懒汉式单例

2.1 饿汉式单例

饿汉式就是在提前初始化,一般在程序启动的时候初始化,很多时候利用编程语言runtime的启动特性来规避并发问题。比如go语言的init函数。
一般实现如下:

// 饿汉单例
package singleton

type EagerSingleton struct{}

var instance *EagerSingleton

func init() {
	instance = &EagerSingleton{}
}

func GetInstance() *EagerSingleton {
	return instance
}

// 程序入口
package main

import (
	"awesomeProject/singleton"
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			instance := singleton.GetInstance()
			fmt.Printf("instance:%p\n", instance)
		}()
	}
	wg.Wait()
}

// 输出:所有的地址都是一样的
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198
instance:0x100d9f198

饿汉式单例的优点很明显不会有并发问题,因为在一开始程序初始化的时候就实例就是产生了。缺点也很明显,就是浪费资源,如果一个实例一直没用到,就会造成资源的浪费。
所以,为了更好的节约资源,引入了懒汉式单例。

2.2 懒汉式单例

也就是将实例初始化的时机由程序初始化的时候改为了使用到的时候在初始化,这样做的好处有以下两点:

  1. 可以较大的节省资源;
  2. 如果初始化很耗时,将初始化时机和启动分离,可以有效减少启动时间,加速启动。
2.2.1 一般实现

一般实现如下:

// 懒汉式单例
package singleton1

type LazySingleton struct {}

var instance *LazySingleton

func GetInstance() *LazySingleton {
	if instance == nil {
		instance = &LazySingleton{}
	}
	return instance
}

// 程序入口
package main

import (
	"awesomeProject/singleton1"
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(100)
	for i := 0; i < 100; i++ {
		go func() {
			defer wg.Done()
			time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
			fmt.Printf("instance: %p\n", singleton1.GetInstance())
		}()
	}
	wg.Wait()
}

// 在使用go run -race ./main.go执行之后,如图

在这里插入图片描述
如果使用 -race失败,可以参考我的另一篇文章:使用race错误

可以看到是发生了data race,也就是并发会产生问题:具体就是:

func GetInstance() *LazySingleton {
	if instance == nil {
		instance = &LazySingleton{}
	}
	return instance
}
// 在instance=nil判断的是否不是原子性的,可能存在两个goroutine同时走到这一块,然后都进入到条件内,从而导致并发问题。
2.2.2 sync.once实现
// 饿汉式单例
import "sync"

type LazySingleton struct{}

var (
	instance *LazySingleton
	once sync.Once
)

func GetInstance() *LazySingleton {
	once.Do(func() {
		instance = &LazySingleton{}
	})
	return instance
}
// 再次使用go run -race ./main.go,结果如图:

多次运行也不会产生问题:

但是!!!,需要注意的是,如果sync.Once使用的时候Do函数失败了,那么就会导致对象一直为nil。比如:

// 饿汉式单例
package singleton1

import "sync"

type LazySingleton struct{}

var (
	instance *LazySingleton
	once     sync.Once
)

func NewLazySingleton(testMsg string) *LazySingleton {
	if testMsg == "Deny" {
		return nil
	}
	return &LazySingleton{}
}

func GetInstance() *LazySingleton {
	once.Do(func() {
		instance = NewLazySingleton("Deny")
	})
	return instance
}

// 程序入口
package main

import (
	"awesomeProject/singleton1"
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(100)
	msg := []string{"Deny", "Allow", "Limit"}
	for i := 0; i < 100; i++ {
		go func() {
			defer wg.Done()
			time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
			testMsg := msg[rand.Intn(len(msg))]
			fmt.Printf("instance: %p\n", singleton1.GetInstance(testMsg))
		}()
	}
	wg.Wait()
}

如果第一次执行的时候,随机到了Deny就会导致后续一直是nil,限制太大,我们继续看看sync.mutex的实现。

2.2.3 sync.Mutex实现
// 	饿汉式单例
package singleton1

import "sync"

type LazySingleton struct{}

var (
	instance *LazySingleton
	mutex    sync.Mutex
)

func NewLazySingleton(testMsg string) *LazySingleton {
	if testMsg == "Deny" {
		return nil
	}
	return &LazySingleton{}
}

func GetInstance(test string) *LazySingleton {
	mutex.Lock()
	defer mutex.Unlock()
	if instance == nil {
		instance = NewLazySingleton(test)
	}
	return instance
}

// 程序入口
package main

import (
	"awesomeProject/singleton1"
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(100)
	msg := []string{"Deny", "Allow", "Limit"}
	for i := 0; i < 100; i++ {
		go func() {
			defer wg.Done()
			time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
			testMsg := msg[rand.Intn(len(msg))]
			fmt.Printf("instance: %p\n", singleton1.GetInstance(testMsg))
		}()
	}
	wg.Wait()
}

这样即使有一次因为deny为nil,也不会影响后续的执行。但是这种方式仔细想想的话,不管instance有没有被初始化,每次有一个instance来的时候都需要获取一次锁,这样对性能很不友好。因此,可以在此基础上加一层判断,如果instance已经被初始化了,就没必要再获取锁了。

func GetInstance(test string) *LazySingleton {
	if instance == nil {
		mutex.Lock()
		defer mutex.Unlock()
		if instance == nil {
			instance = NewLazySingleton(test)
		}
	}
	return instance
}

有兴趣的可以通过进行一些性能对比测试。

2.2.4 原子引用

主要就是将instance==nil换成了原子引用写法

package singleton1

import (
	"sync"
	"sync/atomic"
)

type LazySingleton struct{}

var (
	instanceRef atomic.Value
	lock        sync.Mutex
)

func NewLazySingleton(testMsg string) *LazySingleton {
	if testMsg == "Deny" {
		return nil
	}
	return &LazySingleton{}
}

func GetInstance(test string) *LazySingleton {
	if instanceRef.Load() == nil {
		lock.Lock()
		defer lock.Unlock()
		if instanceRef.Load() == nil {
			instanceRef.Store(NewLazySingleton(test))
		}
	}
	return instanceRef.Load().(*LazySingleton)
}
2.2.5 极致性能优化

直接去掉锁的影响

package singleton1

import (
	"sync/atomic"
)

type LazySingleton struct{}

var (
	instanceRef atomic.Value
)

func NewLazySingleton(testMsg string) *LazySingleton {
	if testMsg == "Deny" {
		return nil
	}
	return &LazySingleton{}
}

func GetInstance(test string) *LazySingleton {
	if instanceRef.Load() != nil {
		return instanceRef.Load().(*LazySingleton)
	}

	newInstance := NewLazySingleton(test)
	// 如果需要手动释放资源,那么需要判断该cas的返回值,这里就不多说。
	instanceRef.CompareAndSwap(nil, newInstance)
	return instanceRef.Load().(*LazySingleton)
}

3 单例模式的优缺点

从上面也可以看得出来:
优点:
访问控制;节约资源
缺点:
职责过重,不好扩展。

  • 28
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SAO&asuna

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值