1. 什么是单例模式
了解单例模式之前,我们需要先了解什么是设计模式。
设计模式是一种抽象的编程思想,不局限于编程语言,简单来说,就是一些大佬程序猿针对一些典型的场景,给出一些典型的解决方案,只要按照这个方案来,也就是按照给定的设计模式,此时代码写的差,也差不到哪去。
本节讲解的就是设计模式中的其中一种:单例模式。
设计模式本质就是一些规章制度,就比如变量命名的风格,如果这个模块要求使用某某设计模式,可以不用吗?当然可以,大不了被队友喷呗,只要内心足够强大。
让我印象特别深的一件事,我还在上学的时候,班里有个人觉得自己写的代码很厉害,往班级群里发,我一看那代码,我靠,shujuyuan,lianbiao,kehuduanqingqiu,这都什么变量名啊,代码能跑吗?能跑,这如果跟我做同事,我指定得喷他。
单例模式很好理解,单例单例,只能有单个实例嘛,就是一个类只能有实例一个对象嘛,不要理解成这个类只能 new 一次哈,而是这个类压根就不让你 new,但是会提供一个给你获取对象的方法,每次获取都是相同的对象!
单例模式的实现常见的有两种实现方式,分别是饿汉模式,懒汉模式,名字奇怪不要紧,后续会讲解。
2. 饿汉模式的实现
public class SingleHungry {
// 提前创建好对象
private static final SingleHungry instance = new SingleHungry();
// 对外隐藏构造方法
private SingleHungry() {
}
// 对外提供获取这个对象的方法
public static SingleHungry getInstance() {
return instance;
}
}
此时饿汉版本的单例模式就实现完成了,阅读上述代码,我们已经提前创建好了 SingleHungry 的实例对象 instance 了,而且使用 private 关键字修饰了构造方法,也就是对外不提供构造方法了,紧接着又写了一个获取 instance 的方法,这样一来,每次获取到的实例都是同一个实例!
public class ThreadDemo {
public static void main(String[] args) {
SingleHungry instance1 = SingleHungry.getInstance();
SingleHungry instance2 = SingleHungry.getInstance();
System.out.println(instance1.equals(instance2));
}
}
// 打印结果:true
当然如果你尝试用 SingleHungry instance = new SingleHungry();
这样显然是会报错的。
为什么这种方式叫做饿汉呢?由于 SingleHungry 类里面的 instance 变量是被 static 修饰的,表示是类的属性,所有对象共享的,只存在一份,并且这个 instance 是在类加载的时候被创建的,类加载是比较靠前的阶段,给人的感觉就是很着急,饿得慌,所以就叫做饿汉模式。
3. 懒汉模式的实现
懒汉模式则于饿汉模式相反,饿汉不是很着急吗,老早就把对象创建了,而懒汉模式则是你什么时候需要,我再创建,那么很多小伙伴想到了,这不是很简单吗?直接在 getInstance() 加一个判断嘛,具体代码实现如下:
public class SingleLazy {
// 不提前创建好对象
private static SingleLazy instance = null;
// 对外隐藏构造方法
private SingleLazy() {
}
// 对外提供获取这个对象的方法
public static SingleLazy getSingleLazy() {
if (instance == null) {
instance = new SingleLazy();
}
return instance;
}
}
那么对于懒汉模式来说,什么时候调用 SingleLazy.getSingleLazy() 的时候才会创建 SingleLazy 对象,效率显然是比饿汉模式要高的。
4. 多线程使用单例模式
大家可千万不要忘了,咱们的标题可还是多线程呐,如果单例模式仅仅是在单线程,那执行顺序都是唯一的,显然不会出现线程安全的问题,但是由于多线程环境下的随机调度,抢占式执行,这样一来,可能就会出问题。
此时来判断上述饿汉模式和懒汉模式在多线程环境下是否会出现线程安全的问题呢?
这里我们主要来看这两个版本里最主要的 get 方法的实现:
// 饿汉模式
public static SingleHungry getInstance() {
return instance;
}
// 懒汉模式
public static SingleLazy getInstance() {
if (instance == null) {
instance = new SingleLazy();
}
return instance;
}
此时很明显发现,饿汉模式的实现,只要调用 get 方法,就会直接返回,不涉及对 instance 的修改,只涉及读操作,而懒汉模式中涉及到 load,cmp,new,save等,简直就是又有读和写啊。
前面讲线程安全时,涉及到读和写,都有可能出现线程不安全。
就懒汉模式的 getInstance 方法,可以大致分为四个步骤,先读取 instance 的值(load),于 null 做比较(cmp) 条件满足,进行 new 操作(new 也分为好几个步骤),接着在写回 instance(save)。
下面我们就画图模拟两个线程里都调用懒汉模式 getInstance 方法的情况:
上述情况之所以能触发多次 new 操作,本质上是因为比较,读,写这三个操作不是原子的,于是我们就可以通过加锁针对上述 getSingleLazy 方法做优化,保证原子性。
// 懒汉模式
public static SingleLazy getInstance() {
synchronized (SingleLazy.class) {
if (instance == null) {
instance = new SingleLazy();
}
}
return instance;
}
此时我们是解决了原子性的问题,但是这个代码会不会效率很低呢?
每次调用 getSingleLazy 方法的时候,都需要先加锁!加锁操作也是有开销的,那我们有必要每次调用该方法都加锁吗?
其实不用每次都加锁,当 instance 为 null 的时候,需要 new 对象的时候,才需要加锁,只要第一次 new 过对象了,后续都不需要加锁了,因为这个类只能实例一个对象,于是我们就可以在前面再次加上判断语句:
public static SingleLazy getInstance() {
if (instance == null) {
synchronized (SingleLazy.class) {
if (instance == null) {
instance = new SingleLazy();
}
}
}
return instance;
}
写到这里,我们还得思考,这个代码是否有内存可见性的问题,假设同一时间很多线程里面都调用了 getSingleLazy 方法,此时只能有一个线程在进行 new 操作,其他线程都在等待锁释放,那么那么多线程读 instance 发现都是 null。
当锁释放,另一个线程获取到锁,前面发现第一次 if 里面读的 instance 为 null,进入第二个 if 的时候,就没有从内存中再次读 instance,而是直接去 寄存器/cache 里读,所以仍然判断 instance 为 null,也会出现重复 new 对象的操作。
再者,instance = new SingleLazy(); 这个操作可以大致拆分成三个步骤:
1. 申请内存空间
2. 调用对应构造方法,初始化内存空间
3. 把内存空间的地址赋值给 instance 引用
如果编译器优化,指令重排序了,本来是按照 1 2 3 的顺序,如果优化成了 1 3 2 的顺序,在多线程的情况,1 2 3 顺序和 1 3 2 是一样的结果,但是多线程环境可不一定了!
假设 t1 线程按照 1 3 2 的顺序去执行,执行完 3 这个操作后,此时 instance 里只是存了一个地址,但地址对应的内存空间并没有初始化,此时如果 t1 被 CPU 切走了(CPU调用其他线程了),CPU 开始调用 t2 线程执行 1 2 3 顺序,此时 t2 就发现 instance 里不为 null,直接 return instance; 但是这个 instance 对应的内存空间并未初始化,所以 t2 调用 getInstance() 得到的就是一个非法的对象(未初始化的对象)。
如上可知,我们要避免出现内存可见性和指令重排序的问题,这时就可以用我们前面学到过的 volatile 关键字,来保证内存可见性,禁止指令重排序。
最终完整版的懒汉模式代码如下:
public class SingleLazy {
// 不提前创建好对象
private volatile static SingleLazy instance = null;
// 对外隐藏构造方法
private SingleLazy() {
}
// 对外提供获取这个对象的方法
public static SingleLazy getSingleLazy() {
if (instance == null) {
synchronized (SingleLazy.class) {
if (instance == null) {
instance = new SingleLazy();
}
}
}
return instance;
}
}
当然这个代码还不完整!虽然我们构造方法是私有的,但是利用反射仍然可以构造多个对象,这里可以采用枚举来防止被反射,还有防止序列化的情况,但是反射是非常规编程手段,如果被面试官要求写个懒汉模式,其实写到这,已经够了!
下期预告:【多线程】生产者消费者模型