我学习设计模式-单例模式
众所周知,很多优秀的框架离不开所谓的设计模式,它不仅能让我们的代码更加易读、健壮、可扩展,还能成功的让我们装杯... 扯多了
当然,学习设计模式,能提高和扩展我们的代码设计思想,丰富我们的技术栈,同时能有效的解决工作中可能出现的问题,岂不美哉嘛。
现在,我们就学习单例?我还依稀记得自己秋招常背诵的几个设计模式之一-单例模式,当然我可不止背了一个,哈哈哈。
但,现在工作了,想再一次认识一下优秀框架源码中经常出现的单例等模式。
背景
何为单例模式?整个程序运行过程中,只存在一个实例对象,其实就这么简单...
但是我们什么场景下会用到呢?什么问题产生了,导致我们需要用单例模式?这是学习一个技术或者知识的一个本质?动力?
那就扯一下有它的好处:
在内存中只有一个实例对象,节省内存空间,避免重复的创建和销毁对象,可以提高性能,避免对多重资源的重复占用,可以全局进行访问。
那什么场景会用到的呢?
需要频繁的实例化和销毁的对象,有状态的工具类对象,频繁访问数据库或文件对象,比如:打印机、数据库连接池、日志管理和应用配置等。
但是总感觉还缺点什么?可能还想再探讨一下
在面向对象语言中,大家都知道静态方法和非静态方法,在内存里其实都放在Method Table里了,在一个类第一次被加载的时候,它会在Loader Heap里把静态方法,非静态方法都写入Method Table中,而且Loader Heap不受GC控制,所以一旦加载,GC就不会回收,直到AppDomain卸载。
还有,非静态方法在创建实例对象时,因为属性的值对于每个对象都各不相同,因此在new一个实例时,会把这个实例属性在GC Heap里拷贝一份,同时这个new出来的对象放在堆栈上,堆栈指针指向了刚才拷贝的那一份实例的内存地址上。
而静态方法则不需要,因为静态方法里面的静态字段,就是保存在Method Table里了,独一份。
因此静态方法和非静态方法,在调用速度上,静态方法速度显然相对快一些,因为非静态方法需要实例化,分配内存,但静态方法不用,但其实这种速度上差异可以忽略不计。
我们不妨思考一下,如果我们全部用静态方法,不用非静态方法,不是一样能实现功能吗?是的,没错,但是你的代码是基于对象,而不是面向对象的,因为面向对象的继承和多态,都是非静态方法。(说白了,就是一种模式上的区别,或者模式上的不同)
再者,不建议全部使用静态方法,假设如果多线程的情况下,如果静态方法使用了一个静态字段,这个静态字段可以会被多个线程修改,因此说如果在静态方法里使用了静态变量,这就可能会出现线程安全问题,当然了,就算不是多线程,因为静态字段只有一份,同样会有被其他地方修改的问题。
扯到关键问题了,为什么使用单例模式而不用静态方法?
如果一个方法和他所在类的实例对象无关,那么它就应该是静态的,反之他就应该是非静态的。如果我们确实应该使用非静态的方法,但是在创建类时又确实只需要维护一份实例时,就需要用单例模式了。
举个例子,比如系统加载一些配置信息和属性,这些配置和属性是一定存在了,又是公共的,同时需要在整个生命周期中都存在,所以只需要一份就行,这个时候如果需要我再需要的时候new一个,再给他分配值,显然是浪费内存并且再赋值没什么意义,因此就体现单例的重要性了。
Java单例的实现
对于Java语言来说,有两种构建方式
- 饿汉,你可以理解为这哥们非常饿,(先把面包煮好了,放在盘子里,等我要的时候,我直接从盘子里拿,开吃)
- 懒汉,你可以理解为懒加载,(材料准备好了,等你饿了需要的时候,我开始做面包,做好,给你就是了,这哥们很稳)
这两种构建方式,有相同的几点要注意:
你可以理解为步骤
- 构造函数,必须私有,就是防止你肆意new对象,我给你关小黑屋里,你就没办法在那疯狂new
- 此实例变量为全局static
- 此方法为全局static
饿汉
根据上面的条件,直接开整:
public class Singleton {
// 首先静态实例变量
private static Singleton uniqueInstance = new Singleton();
// 其次私有构造器
private Singleton(){}
// 最后静态方法,返回静态实例变量
public static Singleton getInstance() {
return uniqueInstance;
}
}
复制代码
是不是感觉饿汉很简单的嘛...
so?JVM在加载这个类时就马上创建此唯一的单例实例,不管你用不用,先创建了再说,如果一直没有被使用,便浪费了空间,典型的空间换时间,每次调用的时候,就不需要再判断,节省了运行时间。
懒汉(缺陷版本)
这哥们出现的问题最多,也是面试常考的一个,最讨厌了...
先来个原始版本的懒汉,一步一步抛出他的问题所在...
public class Singleton {
// 老规矩,三步走
private static Singleton uniqueInstance;
private Singleon(){}
public static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
复制代码
我们来分析一下:
不妨假设有两个线程A、B来调用getInstance的静态方法
首先A呢,刚走到了uniqueInstance = new Singleton();
,突然手机掉地上了,还没来及new结束呢,准备弯腰捡手机,这时候B判断uniqueInstance
确实== null
,然后也走到了A的那边,突然掉进大坑了,好尴尬...,A捡钱结束了,return了uniqueInstance
,此时这个实例变量指向一个地址为0x1
(纯属虚谷)。
B被好心人救上来之后,发现自己还有上头给的任务呢,赶紧new了一个,赶紧跑,此时这个实例变量指向一个地址为0x2
,指向的并不是原来单纯可爱的0x1
了,可恶...
不过,大家很容易想到一种解决方式:就是在getInstance() 方法前加上synchronized关键字,如下
懒汉(还行版本)
public static synchronized Singleton getInstance() {
if (instance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
复制代码
难道真有人不知道synchronized
是什么吧?它就好比你你去厕所蹲马桶,打开厕所的门,反锁一下,别人就进不来了,反锁->synchronized
。
上面的代码,直接在入口方法上了一把反锁,先到先得,等那位哥们上完,你才能上...
但是,是不是觉得不优雅?或者性能比较差,毕竟synchronized关键字偏重量级锁,难道还有人不知道?
虽然在JavaSE1.6之后synchronized关键字进行了主要包括:为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升。
但是,在程序中每次使用getInstance() 都要经过synchronized加锁这一层,这难免会增加getInstance()的方法的时间消费,而且还可能会发生阻塞。
懒汉(双重校验,面试也是常考的一个版本...)
双重校验的含义,你可以暂时的理解为双if
public class Singleton {
//1. mark
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getInstance() {
//2. mark:检查实例,如果不存在,就进入同步代码块
if (uniqueInstance == null) {
//3. mark:只有第一次才彻底执行这里的代码
synchronized(Singleton.class) {
//4. mark:进入同步代码块后,再检查一次,如果仍是null,才创建实例
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
复制代码
上处代码,有四处mark,分别解释一下,你面试的时候这4点要讲清楚呀...
- mark: volatile
大家都知道volatile的几个特点:一、内存可见,二、禁止指令重排
一、内存可见,这和JMM内存架构有点关系,一切为了追求快,追求cpu的利用率,而不是疯狂的在等待... 所以,都会构造一个工作内存,但如何保证多个工作内存中的变量可见,那就利用总缓存的MESI缓存一致性协议的特点来维持了... 有点复杂了,你就暂时理解为volatile能抹掉别人的工作内存的当前变量值,让他们再去总缓存中去获取一遍...
二、禁止重排,当在单线程不影响结果的情况下,打乱指令的顺序,可以提高一定的速度和效率,但是在多线程的情况下,结果有可能不一致,为了保证一致,那么禁止重排,毕竟你要知道new Singleton()
,并不是原子性... 底层分了三步:
memory =allocate(); //1. 分配对象的内存空间
ctorInstance(memory); //2. 初始化对象
instance = memory; //3. 设置instance指向刚分配的内存地址
复制代码
上面三个指令中,步骤2依赖步骤1,但是步骤3不依赖步骤2,所以JVM可能针对他们进行指令重拍序优化,重排后的指令如下:
memory =allocate(); //1. 分配对象的内存空间
instance = memory; //3. 设置instance指向刚分配的内存地址
ctorInstance(memory); //2. 初始化对象
复制代码
这样优化之后,内存的初始化被放到了instance分配内存地址的后面,这样的话当线程1执行步骤3这段赋值指令后,刚好有另外一个线程2进入getInstance方法判断instance不为null,这个时候线程2拿到的instance对应的内存其实还未初始化,这个时候拿去使用就会导致出错。
- mark:第一个if
还不是因为synchronized比较笨重,锁了代码块嘛,多线程不能每次都要进来块中,岂不是都要发生阻塞等这class的锁呀,直接给他上面判断一下不为空就直接跳出去了。提高了性能哇。
-
mark:synchronized,不再细说
-
mark:第二个if
假设没有第二个if,如果A在new对象,B在那恰好在等你释放锁,A释放锁之后,B会进来,直接再new一次,如果有第二个if,扭头就走了,就不会再new啦...
懒汉(静态内部类)
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
复制代码
只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance(只有第一次使用这个单例的实例的时候才加载,同时不会有线程安全问题)。
饿汉(枚举)
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。 它更简洁,自动支持序列化机制,绝对防止多次实例化 (如果单例类实现了Serializable接口,默认情况下每次反序列化总会创建一个新的实例对象。
public enum Singleton {
//定义一个枚举的元素,它就是 Singleton 的一个实例
INSTANCE;
public void doSomeThing() {
System.out.println("枚举方法实现单例");
}
}
复制代码
如何使用:
public class ESTest {
public static void main(String[] args) {
Singleton singleton = Singleton.INSTANCE;
singleton.doSomeThing();//output:枚举方法实现单例
}
}
复制代码
Golang
过多的原因,就不在细说了,大家可以看看一步一步的演变
有缺陷的版本
type singleton struct {}
var instance *singleton
func GetInstance() *singleton {
if instance == nil {
instance = &singleton{} // 不是并发安全的
}
return instance
}
复制代码
上点锁,先解决问题再说
var mu Sync.Mutex
var instance *singleton
func GetInstance() *singleton {
mu.Lock() // 如果实例存在没有必要加锁
defer mu.Unlock()
if instance == nil {
instance = &singleton{}
}
return instance
}
复制代码
有点重,锁的粒度有点粗,那么就细一下,想细,就要双重校验
var mu Sync.Mutex
var instance *singleton
func GetInstance() *singleton {
if instance == nil { // 不太完美 因为这里不是完全原子的
mu.Lock()
defer mu.Unlock()
if instance == nil {
instance = &singleton{}
}
}
return instance
}
复制代码
那就再完美一下
import "sync"
import "sync/atomic"
var initialized uint32
var instance *singleton
... // 此处省略
func GetInstance() *singleton {
if atomic.LoadUInt32(&initialized) == 1 { // 原子操作
return instance
}
mu.Lock()
defer mu.Unlock()
if initialized == 0 {
instance = &singleton{}
atomic.StoreUint32(&initialized, 1)
}
return instance
}
复制代码
但是代码有点复杂了,能不能简单点,确实有简单的,借助Sync.Once的Do方法
package singleton
import (
"sync"
)
type singleton struct {}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
复制代码
注意:这里如果第一次Do失败了,可以设计一个重试机制的挽救措施...
补充Once的源码
// Once is an object that will perform exactly one action.
type Once struct {
// done indicates whether the action has been performed.
// It is first in the struct because it is used in the hot path.
// The hot path is inlined at every call site.
// Placing done first allows more compact instructions on some architectures (amd64/x86),
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 { // check
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock() // lock
defer o.m.Unlock()
if o.done == 0 { // check
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
复制代码