目录
什么是单例模式?
是一种给常见的设计模式,先来他谈谈何为设计模式,在代码领域里,很多程序员的水平参差不齐,于是就有大佬们根据一些常见的需求,整理出来的一些应对办法;那么单例就是指单个实例(对象),也就是说一个类只能有一个实例,例如,在中国,一个男人只能娶一个老婆是一个道理;
单例模式,本质上就是借助变成语言的语法特性,强制限制某个类,不能创建多个实例。
在Java中有些东西是天然的单例,例如static,他可以修饰成员/属性,也就是我们口中熟知的类成员/类属性,实际上这种叫法,也是有一定原因的,也就是这个类特有的成员和属性;更具体的来说,类对象是通过JVM针对某个.class文件只会加载一次,就只有一个类对象,包括类成员,都是靠static修饰,也就只有一份;
饿汉模式
这个模式表示一个类在加载的时候就创建好实例了,“饿汉”一词便体现出创建这个实例是非常急迫,非常早的;
来看看具体代码:
class Singleton{
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton(){
}
}
分析:
用private修饰就是为了防止在类外对Singleton实例进行修改,static修饰保证了这个类无论加载多少次,都可以保证这个实例的创建只被加载一次;这里的instance便是Singleton的唯一实例,在类加载的时候,便已经创建好了
这里便限制了如何拿到instance这个实例——只能通过getInstance这个方法才能拿到这个实例;
将构造方法设置为private,之后在类外,就无法通过new再实例对象了(如下图),所以之后要使用这个唯一的实例,只能通过getInstance方法获得;
懒汉模式
还有一种经典的单例模式叫“懒汉模式”,这里的“懒”是一个褒义词,创建实例来的更迟,但是效率更高;为什么效率更高呢?一般程序刚启动的时候,要初始化的东西有很多,系统资源紧张,所以构造实例这个过程实际上可以往后放放,当这个创建实例的过程晚了,跟其他耗时操作岔开了,初始化效率就高了,速度自然也就跟上了
代码如下:(跟多线程还没扯上关系,不是最优版本)
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance == null){
instance = new SingletonLazy();
}
return instance;
}
}
分析:
刚开始是并没有直接创建实例,而是通过赋值null一笔带过,在真正需要实例的时候,通过调用getInstance方法来获取实例,若instance为空的时候,才去创建实例,不为空的时候说明实例已经创建好,直接返回即可
懒汉模式最佳写法:
class SingletonLazy{
private volatile static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance == null) {
synchronized(SingletonLazy.class){
if(instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
}
以下将会一步一步分析这样写的原因
懒汉和饿汉,谁线程安全,为什么?
咱们可以先来对比一下双方获取实例的方法:
饿汉模式
这个操作只是单纯的“读操作”,不涉及修改;
懒汉模式
这个操作即设计到读操作(装载,比较),也涉及到修改操作(new,赋值),在多线程情况下,就不安全了;
如下图:(创建对象简化为:NEW,赋值简化为:ASSGIN)
分析(脏读):线程二在LOAD操作的时候,线程一还没有修改完,线程二读到还是旧数据,导致线程二CMP时,instance依然是null,所以依然可以NEW,最后实例就被创建了多份!而我们的需求是,让线程二读到的数据是线程一修改完后的数据…
如何修改,让懒汉模式也线程安全?
把多个操作打包成一个操作(原子操作)——加锁;
如下代码:(还不是最高效的)
class SingletonLazy{ private static SingletonLazy instance = null; public static SingletonLazy getInstance(){ synchronized(SingletonLazy.class){ if(instance == null){ instance = new SingletonLazy(); } } return instance; } }
分析:
通过这种加锁方式,就保证了线程安全(如下图)
但是有引入了新的问题…
每次都需要先加锁后,再判断是否创建实例,而加锁操作操作的开销实际上还挺大,加锁可能会涉及到 用户态->内核态 之间的切换,这样的切换成本是很高的,如何解决呢?再来分析一下刚刚的代码:懒汉式代码线程不安全,不是一直线程都不安全,而是在第一次调用的时候才会触发不安全,一旦实例创建好了,就不会有线程不安全问题了,也就是说,加锁操作,只需要在第一次调用的时候加锁即可!所以此时可以再加一个if语句;(如下图,还不是最高效,最后一张图才是)
面试题1:上图中的两个if一模一样,为什么要判断两遍?
第一个if是为了判断是否要加锁,第二个是用来判断是否创建实例,而这两个条件碰巧写法一样,但所表示的意义不同,为什么呢?这两代码中间隔着一个加锁操作,看起来是只隔了一行代码,实际上确隔了孙悟空一个跟头,加锁就可能产生竞争,竞争就会导致阻塞,一旦阻塞,什么时候唤醒就不知道了,所以再个阻塞时间里,一旦线程二创建instance实例,这个时候线程一的第二个if判断就不可省去,也就是说,第二个if和第一个if的结果可能是截然不同的,第一个if成立了,第二个if不一定成立;
面试题2:下图中的volatile有什么用?(懒汉模式完整代码)
解释原因:
假设这样一个场景,俩个线程同时调用getInstance方法,第一个线程拿到了锁,进入第二个if,开始new操作,而这里的new操作实际上是三步操作:
1.申请内存,得到内存的首地址;
2.调用构造方法,来初始化实例;
3.把内存的首地址赋值给instance引用;
这样一个场景之下,可能就会触发指令重排序,例如执行顺序变成了1、3、2 ,再单线程下,3和2是可以相互调换顺序的,没有什么影响,但是在刚刚假设的场景之下调换了3和2的顺序,就有问题了;假设此时触发了指令重排序,按照1、3、2的顺序执行,那么如果线程2在线程1执行了1,3之后(得到了一个空的对象,只分配了内存,数据是无效的),执行2之前,调用了getInstance方法,这时线程2就会进入第一个if,判断instance为非空,就返回了instance,而后续操作一旦涉及到对instance进行访问,就会得到无效数据;这里便是指令重排序带来的问题,要解决这个问题,就需要用到volatile,这个关键字既能保证内存可见性,也能禁止指令重排序,上述代码,就完成了完整的单例模式的懒汉实现
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/124327.html