前言:
大家好,我是Felix。单例在java的面试中,其实属于是被问烂了的话题了,但是为啥呢,就要问这个呢。因为它其实能够考察java相关的知识非常多。我曾经就在拼多多一面中被要求手写单例模式,并口头描述相关原因。那我们今天来缕一缕这个单例模式吧。
- 什么是单例模式
在我们的应用中,绝大多数的Bean对象都属于是无状态的对象。无状态:指的就是多个用户线程来访问这个Bean, 返回同一个bean对象,对于业务没有任何影响。所以说,我们应该对外提供的这类bean就只给一个,这样,大大节省了我们的内存空间,应用也是比较高效的。
其实,我们在学习spring框架的时候,就学习过bean的作用域,常见的就有singleTon和protocal,我们大多数也是采用单例的。由于spring帮我们管理好了这些bean,所以很多时候我们不太需要自己手写单例模式了。
PS: 在Java中,单例模式一般遵循这种编码规范
- 私有化构造方法(不允许别人去new 我这个对象)
- 对外提供一个方法,返回bean
2. 单例模式的分类
主要分为两类,懒汉式与饿汉式,下面我先大体的描述一下两种,后续的代码中再去完善。
1. 饿汉式
可以理解为是一个勤劳的人,很喜欢未雨绸缪,在类初始化的时候就已经完成了bean的创建了,看如下代码,也很好理解。
/**
* 饿汉式
*/
public class EHan {
//类在初始化的时候,bean已经创建好了
private static EHan instance = new EHan();
private EHan() {
}
public static EHan getInstance() {
return instance;
}
}
好处:应用起来后,bean已经创建好了,保证了运行时候的性能,也不会有线程安全相关的问题
坏处:不能保证每个bean在应用期间都会被用到,可能造成内存的浪费,其实就是内存泄露问题。
2. 懒汉式
可以理解为一个懒惰的人,就是拖延症了,可以说是鼻涕不到嘴边不擦,哈哈哈,所以只有真的有人用我才来给你创建。可以看如下代码。
/**
* 懒汉式
*/
public class LHan {
//类加载的时候,bean其实式空的
private static LHan instance;
//被调用时候才会创建
public static LHan getInstance() {
if (null == instance) {
instance = new LHan();
}
return instance;
}
private LHan() {
}
}
好处:用到什么,创建什么,对于内存很友好
坏处:高并发的情况下,很多时候就不一定式单例了,而且还有可能是null,一下会详细说明
- 懒汉式单例存在的问题,如何去解决呢
很多时候,内存是非常珍贵的资源,所以一般我们都是采用懒汉式的单例模式。但是,懒汉式的会有啥问题,如何解决呢?
1. 高并发情况下会有多个bean产生
如下图,想象一下,如果两个线程同时到达第13行代码,此时,由于bean是null,所以都会去创建,那如果是秒杀场景下呢,成千上万的线程就会过来,那内存很有可能极大的浪费了,系统性能也会下降。
如何解决呢?理所当然我们会想到锁呀,看代码
/**
* 懒汉式
*/
public class LHan {
private static LHan instance;
public static LHan getInstance() {
//对于进来的第一个线程,拿到锁了再去实例化
synchronized (LHan.class) {
if (null == instance) {
instance = new LHan();
}
}
return instance;
}
private LHan() {
}
}
2. 影响性能
这个确实可以解决,但是,还是有问题的呢。因为bean的初始化确实满足单例,但是后续的使用,有没有发现每次都要上锁,这个性能影响太大了。继续优化如下。
/**
* 懒汉式
*/
public class LHan {
private static LHan instance;
public static LHan getInstance() {
//这里,如果说我的bean没有创建,那我才去加锁创建,否则,直接返回
if (null == instance) {
synchronized (LHan.class) {
if (null == instance) {
instance = new LHan();
}
}
}
return instance;
}
private LHan() {
}
}
小结: 其实单例模式写到这里,代码层面已经没有问题了,但是还是有坑。我说这个bean有可能拿到的是null,你相信嘛?
3. 指令重排会引起bean为null
* 什么是指令重排
指令重排是指,编译器或者处理器为了优化性能,会将计算机的指令进行重新排序,并行执行。可以这样简单的理解。
单线程模式下,指令重排不会对我们的代码有什么影响,但是多线程下就会有问题。
* Bean的创建过程
+ 实例化:就是去堆中开辟空间
+ 初始化:给bean中的属性进行赋值
+ 暴露引用:把地址值赋值给我们的变量,在这里就是instance
由于指令重排的存在,第二步,第三步很有可能就是反的,那这样,我们的bean其实就是未初始化的一个bean了。
那如何解决呢?
答案:使用java的关键字,volatile
它的作用就是禁止指令重排,使得第三步一定是在第二步后面。那我们看看最终的代码吧。
/**
* 懒汉式
*/
public class LHan {
//这里,加上了volatile关键字!
private static volatile LHan instance;
public static LHan getInstance() {
//这里,如果说我的bean没有创建,那我才去加锁创建,否则,直接返回
if (null == instance) {
synchronized (LHan.class) {
if (null == instance) {
instance = new LHan();
}
}
}
return instance;
}
private LHan() {
}
}
总结:没想到一个小小的单例模式竟然有这么多学问在里面。但是,我们工作中几乎不会去这样写,其实可以用枚举,它是非常好的单例,哈哈哈哈哈,发现说了这么多,竟然工作中不用,但是学习就是这样,量变产生质变。今天的分享到此结束啦!