46

彬哥笔记--21 Go语言单利模式

 5 years ago
source link: https://studygolang.com/articles/18675?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

大家好,我是彬哥,本节给大家讲下go语言设计模式相关,抛砖引玉了,主要是针对Go语言单利使用。

原文链接地址

单例解决了什么?

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

复制代码

这个解释足够简单。说白了就是假如我们希望我们在我们的系统中该类仅仅存在1个或0个该类的实例。虽然单例模式很简单,但是熟悉java的同学可能了解,单例模式有很多写法,懒汉式、饿汉式、双重锁。。。 这么多形式,难道有什么目的?确实,不过他们的目的很明确,就是保证在一种特殊情况下的单例-并发。

ok,既然了解了单例模式,那下面我们就开始用代码描述一下单例模式。首先是最简单的单例,这里我们并不去考虑并发的情况。

package manager
import (
    "fmt"
)

var m *Manager

func GetInstance() *Manager {
    if m == nil {
        m = &Manager {}
    }
    return m
}

type Manager struct {}

func (p Manager) Manage() {
    fmt.Println("manage...")
}

这就是一个最简单的单例了,对于Manager结构体,我们提供了一个GetInstance函数去获取它的实例,这个函数中首先去判断m变量是否为空,如果为空才去赋值一个Manager的指针类型的值,一个小小的判断,就保证了我们在第第二次调用GetInstance的时候直接返回m,而不是重新获取Manager的实例,进而保证了唯一实例。

上面的代码确实简单,也实现了最简单的单例模式,不过大家有没有考虑到并发这一点,在并发的情况下,这里是不是还可以正常工作呢? 来,先跟着下面的思路走一走,来看看问题出现在哪。

现在我们是在并发的情况下去调用的 GetInstance函数,现在恰好第一个goroutine执行到m = &Manager {}这句话之前,第二个goroutine也来获取实例了,第二个goroutine去判断m是不是nil,因为m = &Manager{}还没有来得及执行,所以m肯定是nil,现在出现的问题就是if中的语句可能会执行两遍!

复制代码

在上面介绍的这种情形中,因为m = &Manager{}可能会执行多次,所以我们写的单例失效了,这个时候我们就该考虑为我们的单例加锁啦。

这个时候我们就需要引入go的锁机制-sync.Mutex了,修改我们的代码,

package manager
import (
    "sync"
    "fmt"
)

var m *Manager
var lock *sync.Mutex = &sync.Mutex {}

func GetInstance() *Manager {
    lock.Lock()
    defer lock.Unlock()
    if m == nil {
        m = &Manager {}
    }
    return m
}

type Manager struct {}

func (p Manager) Manage() {
    fmt.Println("manage...")
}

代码做了简单的修改了,引入了锁的机制,在GetInstance函数中,每次调用我们都会上一把锁,保证只有一个goroutine执行它,这个时候并发的问题就解决了。不过现在不管什么情况下都会上一把锁,而且加锁的代价是很大的,有没有办法继续对我们的代码进行进一步的优化呢? 熟悉java的同学可能早就想到了双重的概念,没错,在go中我们也可以使用双重锁机制来提高效率。

package manager
import (
    "sync"
    "fmt"
)

var m *Manager
var lock *sync.Mutex = &sync.Mutex {}

func GetInstance() *Manager {
    if m == nil {
        lock.Lock()
        defer lock.Unlock()
        if m == nil {
            m = &Manager {}
        }
    }

    return m
}

type Manager struct {}

func (p Manager) Manage() {
    fmt.Println("manage...")
}

代码只是稍作修改而已,不过我们用了两个判断,而且我们将同步锁放在了条件判断之后,这样做就避免了每次调用都加锁,提高了代码的执行效率。

这获取就是很完美的单例代码了,不过还没完,在go中我们还有更优雅的方式去实现。单例的目的是啥?保证实例化的代码只执行一次,在go中就中这么一种机制来保证代码只执行一次,而且不需要我们手工去加锁解锁。对,就是我们的sync.Once,它有一个Do方法,在它中的函数go会只保证仅仅调用一次!再次修改我们的代码,

package manager
import (
    "sync"
    "fmt"
)

var m *Manager
var once sync.Once

func GetInstance() *Manager {
    once.Do(func() {
        m = &Manager {}
    })
    return m
}

type Manager struct {}

func (p Manager) Manage() {
    fmt.Println("manage...")
}

代码更简单了,而且有没有发现-漂亮了!Once.Do方法的参数是一个函数,这里我们给的是一个匿名函数,在这个函数中我们做的工作很简单,就是去赋值m变量,而且go能保证这个函数中的代码仅仅执行一次!

单利模式的优缺点和使用场景

优点:

1.在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例

2.单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。

3.提供了对唯一实例的受控访问。

4.由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。

5.允许可变数目的实例。

6.避免对共享资源的多重占用。

缺点:

1.不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。

2.由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。

3.单例类的职责过重,在一定程度上违背了“单一职责原则”。

4.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

使用注意事项:

1.使用时不能用反射模式创建单例,否则会实例化一个新的对象

2.使用懒单例模式时注意线程安全问题

3.饿单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式模式)

适用场景:

单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:

1.需要频繁实例化然后销毁的对象。

2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。

3.有状态的工具类对象。

4.频繁访问数据库或文件的对象。

以下都是单例模式的经典使用场景:

1.资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。

2.控制资源的情况下,方便资源之间的互相通信。如线程池等。

应用场景举例:

1.外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件

2. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~

3. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

4. 网站的计数器,一般也是采用单例模式实现,否则难以同步。

5. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。

6. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。

7. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。

8. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。

9. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。

10. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.

实现单利模式的原则和过程:

1.单例模式:确保一个类只有一个实例,自行实例化并向系统提供这个实例

2.单例模式分类:饿单例模式(类加载时实例化一个对象给自己的引用),懒单例模式(调用取得实例的方法如getInstance时才会实例化对象)(java中饿单例模式性能优于懒单例模式,c++中一般使用懒单例模式)

3.单例模式要素:

a.私有构造方法

b.私有静态引用指向自己实例

c.以自己实例为返回值的公有静态方法

游戏开发中:

单例模式在游戏中的运用十分广泛,在客户端中每个界面可能都是个单例模式,这样用户频繁关闭、打开不会每次都向系统申请空间。在做端游的时候,由于界面上含有不少的图片和动画,如果每次都从本机加载这些文件,代码也是蛮大的。

在服务器中有数据库管理文件,我们知道从数据库中加载文件相对来说还是比较慢的,我们可以一次加载完成,以后每次都使用这个管理类就可以了。

通过上面的例子我们可以看出,一般使用单例模式的地方,说明每次实例化对象的代价还是蛮大的,这样我们可以只实例化一次。

每天坚持学习1小时Go语言,大家加油,我是彬哥,下期见!如果文章中不同观点、意见请文章下留言或者关注下方订阅号反馈!

社区交流群:221273219

字节教育:

www.ByteEdu.Com

Golang语言社区论坛 :

www.Golang.Ltd

LollipopGo游戏服务器地址:

https://github.com/Golangltd/LollipopGo

社区视频课程课件GIT地址:

https://github.com/Golangltd/codeclass
YBVryym.png!web

Golang语言社区


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK