单列模式是校考中最常考的设计模式之一
啥是设计模式?
设计模式就好比好比向其中的"棋谱",红方当头炮,黑方马来跳。针对红方的一些走法黑方有一些固定的套路。按照套路来走局势就不会吃亏。
软件开发中有很对常见的"问题场景':针对这些场景,大佬总结出了一些固定的套路,按照这个套路来实现代码也不会太吃亏。
单列模式能保证某个程序中只存在唯一一份实例,而不会创建出多个实例。
1.饿汉模式
类加载的同时创建实例
java
class Singleton{
private static Singleton instance = new Singleton;
public static Singleton getInstance(){
return instance;
}
private Singleton(){}
}
在这个类被加载的时候,就会初始化这个 静态成员,实例创建的时机非常早,就是用"饿汉"。 后续在别的代码中,尝试new这个Singleton,就会直接编译报错!! 这个写法如果面对反射,当然是无能为力的。
2.懒汉模式
类加载的时候不创建实例,第一次使用的时候才创建实例
java
class Singleton{
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
private Singleton(){}
}
如果是首次调用getInstance,那么此时instance引用为null,就会进入if条件,从而把实例创建出来。如果是后续再次调用getInstance,由于instance已经不再是null此时就不会在进入if,直接返回之前创建好的引用了。 这样设定,仍然可以保证,该类的实例是唯一一个,与从同时,创建实例的时机就不是程序驱动时了,而是++第一次调用getInstance的时候++ 。这个操作执行的时机就不知道了就看你程序的实际需求,大概率要比饿汉这种要晚一些甚至有可能整个程序压根用不到这个方法,就把创建的操作给剩下了。
在计算机中"懒"这个茨并不是贬义词而是褒义词。懒的思想就非常有意义
比如有一个非常大的文件(10GB)
有一个编辑器,使用编辑器打开这个文件.
如果是按照"饿没"的方式;编辑器就会先把这10-GB的数据都加载到内存中,然后再进行统一-的展示. 即使加载了这么多数据,用户还得一点点看,没法一下子看完这么多
如果是按照"懒汉"的方式,编辑器就会只读取一-小部分数据(比如只读10KB),把这10KB先展示出来.随着用户进行翻页之类的操作,再继续读后续的数据.
3.多线程~~
一个关键问题:上述编写的代码,饿汉模式和懒汉模式,是否线程安全的??
结合之前讨论过的一些线程不安全的一些原因~~
饿汉模式
对于饿汉来说,getInstance直接返回instance实例,整个操作本质上就是"读操作",多个线程读取同一个变量,是不是线程安全的?? 是线程安全的!!
懒汉模式
如果按上述的图列执行这就导致实例new了两次!!,这就不是单列模式了,就有bug了!! 懒汉模式,线程不安全,在多线程环境下可能会创建多个实例!!
懒汉模式改进
如何改进懒汉模式,让他能够成为线程安全的代码?? 加锁,synchronized
java
public static Singleton getInstance(){
if(instance == null){
synchronized(locker){
instance = new Singleton();
}
}
return instance;
}
这样加锁就安全了吗??
多线程代码,其实是非常复杂的,代码稍微变换一点,结论就截然不同!! 因此可千万不要以为,代码中写了synchronized就一定线程安全,不写synchronized就一定线程不安全!!一定要具体问题具体分析,分析这个代码在各种调度执行顺序下可能发生的情况,确保每个情况都是正确的!!
此处想要让代码执行正确,其实是需要把if和new两个打包成一个原子的!!
更合理的做法,应该是把synchronized套到if外头
此时就可以确保,一定是t1执行完new操作,执行完修改Instance之后,再回到t2执行if,t2的if条件就不会成立了,t2就会直接返回了。
上述代码仍然存在一些问题
如果Instance已经创建过了,此时后续再调用getInstance就都是直接返回实例了吧(此处的操作就是纯粹的读操作了,也就不会有线程安全问题了),此时针对这个已经没有线程安全问题的代码,仍然是每次调用都先加锁再解锁,此时,效率就非常低了 !!
加锁就意味着可能会产生阻塞,一旦线程阻塞,啥时候能解除,就不知道(你可以认为,只有一个代码里加锁了,基本就注定和" 高性能 "无缘)。
java
public static SingletonLazy getInstance(){
//如果Instance 为null,就说明是首次调用,首次调用就需要考虑线程安全问题,就要加锁
//如果非null,就说明是后续调用,就不必加锁了
if(instance == null){
synchronized (Locker){
if(instance==null){
instance = new SingletonLazy();
}
}
}
return instance;
}
这俩个if看起来是两个一样的条件,但实际上,这俩条件结果可能是相反的!!
这个synchronized就可能让当前这个线程阻塞,阻塞的过程中就可能有别的线程修改了Instance了!!这俩个if中间隔的时间,可能是沧海桑田。
第一个if判定的是是否要加锁,第二个if判定的是是否要创建对象巧了!!这俩if条件相同了。 线程安全&执行效率 "双重校验锁"
代码仍然有问题
指令重排序,引起的线程安全问题。指令重排序,也是编译器优化的一种方式。
这行代码,其实可以拆成三个大步骤。(不是三个指令)
1.申请一段内存空间
2.在这个内存上调用构造方法,创建出这个实例
3.把这个内存地址赋值给Instance引用变量
正常情况下,上述代码按照1,2,3的顺序来执行的,但是编译器也可能优化成1,3,2的顺序来执行,无论是1,2,3还是1,3,2在单线程下,都是可以的~~
但是,如果是在多线程下,指令重排序,就可能引入问题了!!
t1按照1,3,2的方式来执行这里的new操作
上述代码,有于t1线程执行完1,3之后,在要执行2的时候被调度走,这时候instance指向的是一个非null的,未初始化的对象, 而t2线程判定instance == null不成立,就会直接return。如果t2继续使用instance里面的属性或者方法,就会出现问题(此时这里的属性都是未初始化的"全0"值)就可能会引起代码的逻辑出现问题。
解决方法
解决上述问题,核心思路还是volatile
volatile有两个功能:
1.保证内存可见性,每次访问变量必须都要重新读取内存,而不会优化到寄存器/缓存中。
2.禁止指令重排序,针对这个呗volatile修饰的变量的读写操作相关指令,是不能被重排序的!!
这个时候,针对这个变量的读写操作,就不会出现重排序了,此时执行顺序一定是1,2,3也就杜绝了上述问题。
小结
java
class SingletonLazy{
//这个引用指向唯一实例,这个引用先初始化为null,而不是立即创建实例
private volatile static SingletonLazy instance = null;
private static Object Locker = new Object();
public static SingletonLazy getInstance(){
//如果Instance 为null,就说明是首次调用,首次调用就需要考虑线程安全问题,就要加锁
//如果非null,就说明是后续调用,就不必加锁了
if(instance == null){
synchronized (Locker){
if(instance==null){
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){}
}
1.在合适的位置进行加锁.
2.该加的时候加,不该加的时候不要加,避免效率受到影响避免双重if.
3.通过volatile禁止这里的重排序,避免出现线程安全问题