彬哥Go语言笔记Golang语言社区

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

2019-03-05  本文已影响5人  Golang语言社区

      大家好,我是彬哥,本节给大家讲下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


Golang语言社区
上一篇下一篇

猜你喜欢

热点阅读