介绍
单例模式属于创建型模式,它提供了一种创建对象的最佳方式。在应用程序中,有些情况下需要确保某个类只有一个实例对象,比如数据库连接池、线程池、垃圾回收器、日志记录器等。单例模式可以确保这些类只被实例化一次,并提供一个全局访问点,使其他对象可以方便地访问该实例。
特点
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点。
优点
- 在内存里只有一个实例,调用时复用该单例对象即可,减少了内存的开销,尤其是频繁的创建和销毁实例。
- 避免了对资源的多重占用。
缺点
- 全局状态和共享状态:单例模式引入全局状态和共享状态,可能会增加代码的复杂性和耦合性。修改单例类的行为可能会对整个系统产生影响。
- 难以扩展和测试:由于单例模式将对象的创建和访问耦合在一起,当需要扩展或修改单例类时,可能会面临困难,不利于单元测试和代码维护。
- 违反单一职责原则:单例模式往往承担了太多的职责,既负责创建实例,又负责管理全局状态和提供全局访问点,导致单例类的职责不够单一,违反了单一职责原则。
使用场景
- 资源共享:当多个对象需要共享同一个资源(如数据库连接池、线程池)时,使用单例模式可以确保只有一个实例被创建和共享,避免资源的重复创建和浪费。
- 对象缓存:在需要缓存对象的场景下,使用单例模式可以确保只有一个缓存实例存在,提高缓存的效率和一致性。
- 配置信息:当系统中的某些配置信息需要被多个对象共享和访问时,可以使用单例模式来管理和提供该配置信息的实例。比如唯一序列号。
- 日志记录:单例模式可以用来创建全局的日志记录器,以确保在整个应用程序中只有一个实例记录日志,方便进行统一的日志管理和记录。
- GUI应用程序中的窗口管理:在GUI应用程序中,通常需要管理和控制多个窗口的状态和行为。使用单例模式可以创建一个窗口管理器实例,统一管理窗口的创建、销毁和状态。
- 计数器或计时器:在需要对某个计数器或计时器进行全局控制和访问的场景下,可以使用单例模式来创建该实例,并确保只有一个实例存在。比如web中的计数器。
实现方法
实现方法有两种:懒汉式和饿汉式。这两种的主要区别在于实例的创建时机和线程的安全性。
懒汉式
懒汉式(Lazy Initialization):是指在首次访问时才创建实例。具体来说,当第一次调用获取实例的方法时,判断实例是否创建,如果没有则创建一个实例并返回。如果实例已经存在,则直接返回现有的实例。
优点:
- 延迟加载:只有在需要时才会创建实例,可以节省系统资源。
- 线程安全(通过加锁):可以通过加锁机制确保在多线程环境下只有一个线程创建实例。
缺点:
- 性能受影响:由于需要在获取实例时进行判断和可能的加锁操作,因此可能会对性能产生一定的影响。
- 实现复杂度增加:为了实现线程安全,需要考虑加锁的逻辑,增加了实现的复杂度。
不加锁实现
这种方式是会导致线程不安全的。在高并发的时候会有多个线程同时调用这个方法,那么都会检测instance为nil,这样就会导致创建多个对象,所以这种方法是不推荐的。
注意这里的singleton必须是小写的,如果是大写的,其他包就可以通过s := &singleton{}
来创建多个实例,单例模式就没有意义了。
package singleton
type singleton struct {}
var instance *singleton
func GetInstance() *singleton {
if instance == nil {
instance = &singleton{}
}
return instance
}
整个方法加锁实现
使用Sync.Mutex进行加锁,保证线程安全,这种写法有个最大的缺点就是每次调用该方法时都需要进行锁操作,在性能上相对不高效。
package singleton
import "sync"
type singleton struct {}
var instance *singleton
var lock sync.Mutex // 锁对象
func GetInstance() *singleton {
lock.Lock()
defer lock.Unlock()
if instance == nil{
instance = &singleton{}
//instance = new(singleton)
}
return instance
}
创建实例时加锁实现
虽然加了锁,但多线程还是会创建出多实例,也不推荐。
package singleton
import "sync"
type singleton struct {}
var instance *singleton
var lock sync.Mutex
func GetInstance() *singleton {
if instance == nil{
lock.Lock()
instance = &singleton{}
lock.Unlock()
}
return instance
}
双重检查加锁实现
只有当对象未初始化的时候,才会有加锁和减锁的操作。但是又出现了另一个问题:每一次访问都要检查两次。
package singleton
import "sync"
type singleton struct {}
var instance *singleton
var lock sync.Mutex
func GetInstance() *singleton {
if instance == nil{
lock.Lock()
if instance == nil{
instance = &singleton{}
}
lock.Unlock()
}
return instance
}
原子操作实现
通过sync.Once的Do方法来确保创建对象的方法只执行一次,sync.Once内部本质上也是双重检查的方式,但在写法上会上面更简洁,这样就可以只创建了一个对象,这种方法是推荐的。
package singleton
import "sync"
type singleton struct {
}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
饿汉式
饿汉式(Eager Initialization):是指在类加载时就创建实例,无论是否使用该实例。在类加载过程中,实例就会被创建,并且在整个应用程序生命周期中都存在。
优点:
- 简单直观:实例在类加载时就被创建,使用起来非常简单。
- 无需加锁,更安全。
缺点: - 资源浪费:无论是否使用该实例,都会在应用程序启动时创建实例,可能会浪费一些系统资源,启动速度慢。
- 不支持延迟加载:由于实例在类加载时就被创建,无法实现延迟加载的特性。
全局变量实现
package singleton
type singleton struct {}
var instance *singleton
func GetInstance() *singleton{
return instance
}
init函数实现
init函数在包被加载时自动执行,因此在应用程序启动时就会创建实例并赋值给instance变量。
package singleton
type singleton struct{}
var instance *singleton
func init() {
instance = &singleton{}
}
func GetInstance() *singleton {
return instance
}
两种方法都可以实现,没有什么区别。