单例模式
目录
图解
单例模式采用了 饿汉式 和 懒汉式 两种实现,个人其实更倾向于饿汉式的实现,简单,并且可以将问题及早暴露,懒汉式虽然支持延迟加载,但是这只是把冷启动时间放到了第一次使用的时候,并没有本质上解决问题,并且为了实现懒汉式还不可避免的需要加锁。
单例模式概念
单例模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式,是一种很常见的软件设计模式,在他的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。
单例模式确保某一个类只有一个实例。为什么要确保一个类只有一个实例?有什么时候才需要用到单例模式呢?听起来一个类只有一个实例好像没什么用呢!那我们来举个例子。比如我们的APP中有一个类用来保存运行时全局的一些状态信息,如果这个类实现不是单例的,那么App里面的组件能够随意的生成多个类用来保存自己的状态,等于大家各玩各的,那这个全局的状态信息就成了笑话了。而如果把这个类实现成单例的,那么不管App的哪个组件获取到的都是同一个对象(比如Application类,除了多进程的情况下)。
- 1.单例类只能有一个实例,并提供一个访问它的全局访问点。
- 2.单例类必须自己创建自己的唯一实例。
- 3.单例类必须给所有其他对象提供这一实例。
单例模式优点
- 1.在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
- 2.避免对资源的多重占用。
单例模式应用实例
- 1、一个班级只有一个班主任。
- 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
- 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
单例模式使用场景
- 1.要求生产唯一序列号。
- 2.WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 3.创建的一个对象需要消耗的资源过多时,比如 I/O 与数据库的连接等。
- 4.主要解决"一个全局使用的类频繁地创建与销毁"这样的问题
- 5.当您想控制实例数目,节省系统资源的时候可以用单例模式
单例模式实现方式
单例模式的实现主要有2种方式:
1.懒汉模式
2.饿汉模式
懒汉模式
概念
先说一下什么是懒汉模式吧,从懒汉这两个字,我们就能知道,这个人很懒,所以他不可能在未使用实例时就创建了对象,他肯定会在使用时才会创建实例,这个好处的就在于,只有在使用的时候才会创建该实例。下面我们一起来看看他的实现:
不加锁实现
注意:以下我写的代码,除了GetInstance方法外其他都使用的小写字母开头,原因如下:
golang中根据首字母的大小写来确定可以访问的权限。无论是方法名、常量、变量名还是结构体的名称,如果首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用。可以简单的理解成,首字母大写是公有的,首字母小写是私有的。
这里type singleton struct {}我们如果使用大写,那么我们写的这些方法就没有意义了,其他包就可以通过s := &singleton{}创建多个实例,单例模式就显得很没有意义了,所以这里一定要注意一下哦~~~
不加锁实现:这种方法是会存在线程安全问题的,在高并发的时候会有多个线程同时掉这个方法,那么都会检测instance为nil,这样就会导致创建多个对象,所以这种方法是不推荐的
package one
type singleton struct {
}
var instance *singleton
func GetInstance() *singleton {
if instance == nil{
instance = new(singleton)
}
return instance
}
整个方法加锁
整个方法加锁:这里对整个方法进行了加锁,这种可以解决并发安全的问题,但是效率就会降下来,每一个对象创建时都是进行加锁解锁,这样就拖慢了速度,所以不推荐这种写法。
type singleton struct {
}
var instance *singleton
var lock sync.Mutex
func GetInstance() *singleton {
lock.Lock()
defer lock.Unlock()
if instance == nil{
instance = new(singleton)
}
return instance
}
创建方法时进行锁定
创建方法时进行锁定:这种方法也是线程不安全的,虽然我们加了锁,多个线程同样会导致创建多个实例,所以这种方式也不是推荐的。
type singleton struct {
}
var instance *singleton
var lock sync.Mutex
func GetInstance() *singleton {
if instance == nil{
lock.Lock()
instance = new(singleton)
lock.Unlock()
}
return instance
}
双重检锁
这里在上面的代码做了改进,只有当对象未初始化的时候,才会有加锁和减锁的操作。但是又出现了另一个问题:每一次访问都要检查两次
原子操作实现
为了解决"双重检索中每一次访问都要检查两次"这个问题,我们可以使用golang标准包中的方法进行原子性操作;
这里使用了sync.Once的Do方法可以实现在程序运行过程中只运行一次其中的回调,这样就可以只创建了一个对象,这种方法是推荐的~~~。
type singleton struct {
}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = new(singleton)
})
return instance
}
饿汉模式
概念
有懒汉模式,当然还要有饿汉模式啦,看了懒汉的模式,饿汉模式我们很好解释了,因为他饿呀,所以很着急的就创建了实例,不用等到使用时才创建,这样我们每次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。比较适用于:如果某个单例使用的次数少,并且创建单例消息的资源比较多,那么就需要实现单例的按需创建,这个时候懒汉模式就是一个不错的选择。不过也有缺点,饿汉模式将在包加载的时候就会创建单例对象,当程序中用不到该对象时,浪费了一部分空间,但是相对于懒汉模式,不需要进行了加锁操作,会更安全,但是会减慢启动速度。
全局变量实现、init加载实现
以下这两种方法都可以,第一种我们采用创建一个全局变量的方式来实现,第二种我们使用init包加载的时候创建实例,这里两个都可以,不过根据golang的执行顺序,全局变量的初始化函数会比包的init函数先执行,没有特别的差距。
type singleton struct {
}
var instance = new(singleton)
func GetInstance() *singleton{
return instance
}
type singleton struct {
}
var instance *singleton
func init() {
instance = new(singleton)
}
func GetInstance() *singleton{
return instance
}
代码实现
饿汉式
代码实现:
package singleton
// Singleton 饿汉式单例
type Singleton struct{}
var singleton *Singleton
func init() {
singleton = &Singleton{}
}
// GetInstance 获取实例
func GetInstance() *Singleton {
return singleton
}
单元测试:
package singleton_test
import (
"testing"
singleton "github.com/mohuishou/go-design-pattern/01_singleton"
"github.com/stretchr/testify/assert"
)
func TestGetInstance(t *testing.T) {
assert.Equal(t, singleton.GetInstance(), singleton.GetInstance())
}
func BenchmarkGetInstanceParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if singleton.GetInstance() != singleton.GetInstance() {
b.Errorf("test fail")
}
}
})
}
懒汉式(双重检测)
代码实现:
package singleton
import "sync"
var (
lazySingleton *Singleton
once = &sync.Once{}
)
// GetLazyInstance 懒汉式
func GetLazyInstance() *Singleton {
if lazySingleton == nil {
once.Do(func() {
lazySingleton = &Singleton{}
})
}
return lazySingleton
}
测试结果
可以看到直接 init 获取的性能要好一些
▶ C:\Users\laili\sdk\go1.15\bin\go.exe test -benchmem -bench="." -v
=== RUN TestGetLazyInstance
--- PASS: TestGetLazyInstance (0.00s)
=== RUN TestGetInstance
--- PASS: TestGetInstance (0.00s)
goos: windows
goarch: amd64
pkg: github.com/mohuishou/go-design-pattern/01_singleton
BenchmarkGetLazyInstanceParallel
BenchmarkGetLazyInstanceParallel-4 535702941 2.24 ns/op 0 B/op
0 allocs/op
BenchmarkGetInstanceParallel
BenchmarkGetInstanceParallel-4 1000000000 0.586 ns/op 0 B/op
0 allocs/op
PASS
ok github.com/mohuishou/go-design-pattern/01_singleton 3.161s