一、介绍
单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
本文给出了Java和Golang的代码实现,即使语言不同,思想是相同的,但考虑到不同语言的特性,会存在些许的差别。
二、Java实现单例模式
Java实现单例模式的方式主要有5种,分别是饿汉模式、懒汉模式、双重检测机制实现、静态内部类实现、枚举类实现。
2.1、饿汉模式
代码:
public final class Singleton {
private static Singleton instance = new Singleton("Foo");
public String value;
private Singleton(String value) {
this.value = value;
}
public static Singleton getInstance() {
return instance;
}
}
在类加载的时候就对实例进行初始化,没有线程安全问题;获取实例的静态方法没有使用同步,调用效率高;但是没有使用懒加载,如果该实例从始至终都没被使用过,则会造成内存浪费。
总结:线程安全、非懒加载、效率高。
是否推荐:可以使用,但不推荐。
2.2、懒汉模式
2.2.1、非线程安全
代码:
public final class Singleton {
private static Singleton instance;
public String value;
private Singleton(String value) {
this.value = value;
}
public static Singleton getInstance(String value) {
if (instance == null) {
instance = new Singleton(value);
}
return instance;
}
}
这种实现方式在第一次使用的时候才进行初始化,达到了懒加载的效果,较为节省内存资源。但如果遇到并发情况,会出现线程安全问题。
总结:懒加载,效率低
是否推荐:不推荐。
2.2.2、线程安全
代码:
public final class Singleton {
private static Singleton instance;
public String value;
private Singleton(String value) {
this.value = value;
}
public static synchronized Singleton getInstance(String value) {
if (instance == null) {
instance = new Singleton(value);
}
return instance;
}
}
线程安全的懒汉模式是比较常见的一种写法。由于获取实例的静态方法用synchronized修饰,所以也没有线程安全的问题;但是,这种写法每次获取实例都要进行同步(加锁),因此效率较低,并且可能很多同步都是没必要的。
总结:线程安全、懒加载、效率低。
是否推荐:可以使用,但不推荐。
2.3、双重检测机制
代码:
public final class Singleton {
private static volatile Singleton instance; //注意volatile修饰符
public String value;
private Singleton(String value) {
this.value = value;
}
public static Singleton getInstance(String value) {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(value);
}
}
}
return instance;
}
}
在第一次使用的时候才进行初始化,达到了懒加载的效果;
在进行初始化的时候会进行同步(加锁),没有线程安全问题;并且只有第一次进行初始化才进行同步,因此效率较高。
总结:线程安全、懒加载、效率高。
是否推荐:可以使用。
注:
该模式还有另一种常见写法,就是把静态实例singleton的volatile修饰去掉,这种实现方式在大多数情况下是有效的,
但在极端情况下有线程安全方面的问题,不能使用。
例如:
假设有两个线程同时访问这段代码,此时线程
A
走到13行开始初始化对象,线程B
则刚走到第10行进行第一次检测。注意第13行代码虽然只有一句话,但是被编译后会变成以下3条指令:
memory = allocate(); // 1、分配对象的内存空间 ctorInstance(memory);// 2、初始化对象 instance=memory; // 3、设置instance指向刚才分配的内存地址
在正常情况下,3条指令是按照此顺序执行的,但在CPU内部会在保证不影响最终结果的前提下对指令进行重新排序(不影响最终结果只是针对单线程),指令重排的主要目的是为了提高效率。在本例种,如果这3条指令被重排为以下顺序:
memory = allocate(); // 1、分配对象的内存空间 instance=memory; // 3、设置instance指向刚才分配的内存地址 ctorInstance(memory);// 2、初始化对象
如果线程
A
执行完1和3,instance对象还未完成初始化,但是已经不再指向null。此时线程
B
抢占到CPU资源,执行第10行的检测结果为false,则执行第17行,从而返回一个还未初始化完成的instance对象,从而出导致问题出现。要解决这个问题,因此需要使用volatile关键字修饰instance对象。
2.4、静态内部类
代码:
public final class Singleton {
//静态内部类
private static class SingletonHolder {
private static final Singleton instance = new Singleton("FOO");
}
public String value;
private Singleton(String value) {
this.value = value;
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
JVM将推迟SingletonHolder
的初始化操作,直到开始使用这个类时才初始化,并且由于通过一个静态初始化来初始化Singleton,因此不需要额外的同步。
当任何一个线程第一次调用getInstance
时,都会使SingletonHolder
被加载和被初始化,此时静态初始化器将执行Singleton
的初始化操作。
总结:线程安全、懒加载、效率高。
是否推荐:推荐使用
注:《Java Concurrency in Practice》作者Brian Goetz推荐使用的方式
2.5、枚举类
代码:
public enum Singleton {
INSTANCE("FOO");
public String value;
private Singleton(String val) {
this.value = val;
}
}
前四种单例模式的实现是可以通过反射和序列化/反序列化来破解的,而enum
由于自身的特性问题,是无法破解的。当然,由于这种情况基本不会出现,因此我们在使用单例模式的时候也比较少考虑这个问题。
枚举类实现是很简洁的一种实现方式,提供了序列化机制,保证线程安全,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。
总结:线程安全、非懒加载、效率高。
是否推荐:推荐使用
注:《Effective Java》作者Joshua Bloch推荐使用的方式。
三、Golang实现单例模式
Golang并不是纯粹意义上的面向对象语言,在实现单例模式上与Java并不完全相同。
实现方式也可分为饿汉式、懒汉式、双重检测,除此之外,也可以利用Golang的sync包实现。
3.1、饿汉模式
这种实现方式在大多数情况下并不推荐使用。
3.1.1、普通实现
代码:
package singleton
type singleton struct {
Val string
}
var instance = &singleton{"FOO"}
func NewInstance() *singleton {
return instance
}
这种方式的单例对象是在包加载时立即被创建。
3.1.1、init函数实现
代码:
type singleton struct {
Val string
}
var instance *singleton
func NewInstance() *singleton {
return instance
}
func init() {
instance = &singleton{"FOO"}
}
我们可以在init
函数中创建单例实例。
init
函数仅会在包中的每个文件里调用一次, 所以我们可以确定其只会创建一个实例。
注:init函数可查看此博客:https://blog.csdn.net/bestzy6/article/details/122503275
3.2、懒汉模式
3.2.1、非协程安全:
package singleton
type singleton struct {
Val string
}
var instance *singleton
func NewInstance(v string) *singleton {
if instance == nil{
instance = &singleton{Val: v}
}
return instance
}
懒汉模式,能够懒加载,但会造成协程不安全。
不推荐使用。
3.2.2、协程安全:
package singleton
type singleton struct {
val string
}
var (
instance *singleton
m sync.Mutex
)
func NewInstance(v string) *singleton {
m.Lock()
defer m.Unlock()
if instance == nil {
instance = &singleton{Val: v}
}
return instance
}
协程安全,但加锁操作会降低效率。
不推荐使用。
3.3、双重检测
package singleton
type singleton struct {
Val string
}
var (
instance *singleton
m sync.Mutex
)
func NewInstance(v string) *singleton {
if instance == nil {
m.Lock()
defer m.Unlock()
if instance == nil {
instance = &singleton{Val: v}
}
}
return instance
}
在进行初始化的时候会进行加锁,没有线程安全问题,并且只有第一次进行初始化才进行同步,因此效率较高。
可以使用。
3.4、使用sync.once
package singleton
type singleton struct {
Val string
}
var (
instance *singleton
once sync.Once
)
func NewInstance(v string) *singleton {
once.Do(func() {
instance = &singleton{Val: v}
})
return instance
}
Once 是一个结构体,在执行 Do 方法的内部通过 atomic 操作和加锁机制来保证并发安全,且 once.Do 能够保证多个 goroutine 同时执行时 &singleton {} 只被创建一次。
其实 Once 并不神秘,其内部实现跟上面使用的双重锁定机制非常类似,只不过把 instance == nil 换成了 atomic 操作,感兴趣的同学可以查看下其对应源码。
推荐使用。
四、总结
方式 | 优点 | 缺点 |
---|---|---|
饿汉式 | 线程安全、效率高 | 非懒加载 |
非线程安全懒汉式 | 懒加载、效率高 | 非线程安全 |
线程安全懒汉式 | 线程安全、懒加载 | 效率低 |
双重检测 | 线程安全、懒加载、效率高 | 无 |
静态内部类(Java) | 线程安全、懒加载、效率高 | 无 |
枚举(Java) | 线程安全、效率高 | 非懒加载 |
使用sync.once (Golang) | 线程安全、效率高、懒加载 | 无 |
参考: