前言
提一个问题,大家知道spring容器管理的bean,默认是单例的么,如果是,那为什么用到单例呢, 有什么好处呢。至于spring是如何实现bean的单例我们到spring的章节再细聊,我们今天只聊聊单例模式。
定义
简单来说,就是保证一个系统只有一个实例,只提供了一个对外访问点
特点
- 只有一个实例。
- 自我实例化。
- 提供全局访问点。
优点
由于单例模式只会生成一个实例,自然能够节约系统资源,减少性能开销
缺点
没有接口,也不能继承,违背了单一职责(一个类应该只关心内部的逻辑,而不应该关心外部的实例化)
代码实现
懒汉单例模式
懒汉单例,顾名思义,就是他是个很懒的类,你不主动调用他时,他是不会主动实例化的,我们来看代码
class SingleExample01 {
private static SingleExample01 singleExample;
// 避免通过构造方法创建对象
private SingleExample01() {
}
public static SingleExample01 getInstance() {
if (singleExample == null){
singleExample = new SingleExample01();
}
return singleExample;
}
}
上述代码是线程不安全的,在多线程的情况下,无法保证对象的单例,所以严格来说他不算真正的单例模式
其实我们要想保证线程安全,最简单的方法就是在获取单例方法的上面加synchronized锁,保证每次进入这个方法的只有一个线程
class SingleExample001 {
private static SingleExample01 singleExample;
// 避免通过构造方法创建对象
private SingleExample01() {
}
public static synchronized SingleExample01 getInstance() {
if (singleExample == null){
singleExample = new SingleExample01();
}
return singleExample;
}
}
优点
当对象需要的时候才会被实例化,避免产生垃圾对象,造成内存浪费
缺点
如果不加锁,这种单例模式是线程不安全的,在多线程的情况下,很难保证单例;但是加了锁,就会导致效率低下
饿汉式单例
饿汉单例,顾名思义,就是他很饥渴,不管其他需不需要,我先实例化好,等调用方需要的时候,就不需要再实例化,直接返回给调用方
class SingleExample02 {
// JVM保证线程安全
private static final SingleExample02 singleExample = new SingleExample02();
// 避免通过构造方法创建对象
private SingleExample02() {
}
public static SingleExample02 getInstance() {
return singleExample;
}
}
饿汉式单例天生就是线程安全的,因为被static修饰,在类加载的时候,对象就会被实例化出来,并且只会被实例化一次
优点
不需要加锁,执行效率较高
缺点
可能会创建垃圾对象,造成内存浪费
双检锁(双重校验锁)
这种单例其实也是一种饿汉式单例,不过他保证了线程的安全性。直接上代码吧
class SingleExample03 {
private static volatile SingleExample03 singleExample;
// 避免通过构造方法创建对象
private SingleExample03() {
}
public static SingleExample03 getInstance() {
if (singleExample == null){
synchronized (SingleExample03.class){
if (singleExample == null){
singleExample = new SingleExample03();
}
}
}
return singleExample;
}
}
我们看到上述代码,有没有两个疑问:
- 为啥判断了两次对象为null的情况
- 为啥给对象变量前加volatile关键字
我们一个个来解答
为啥判断了两次对象为null的情况
- 第一个null判断是为了价格前置条件,避免大量线程去争夺锁资源,造成系统不必要的资源浪费
- 第二个null的判断可以这样理解,我们有三个线程A,B,C同时调用getInstance方法,这时候A,b,c都通过了第一个null判断,然后在锁的争夺中,A抢到了锁,B,C等待,A实例化了对象释放了锁。接着B争夺到了锁,如果没有null判断,B就会直接实例化对象,这样就无法保证对象的单例了。
那么只做了两次非空判断就一定能够保证对象的单例了么
为啥给对象变量前加volatile关键字
我们在来说说volatile关键字
首先我们实例化对象的操作并不是原子性的,需要经历三个步骤
- 在堆中开辟对象所需空间,分配地址
- 根据类加载的初始化顺序进行初始化
- 将内存地址返回给栈中的引用变量
由于CPU的优化程序,可能会对我们的指令进行重排,将本来123的执行顺序变为132,这样对象的变量就会指向对应的内存地址,而这时候对象还没有真正的进行初始化。
我们都知道volatile有两个特性
- 禁止指令重排
- 保证可见性
我们假设一个场景,有两个线程A,B,当A进入获取到了锁,根据上面说的重排序问题,A走到了1,3两步,还没有执行2步;这时候B线程进来了,获取单例的时候,发现对象变量不为空,就直接返回了这个还没有初始化的对象。我们在使用对象的时候就会出现问题,这也就是利用volatile禁止指令重排序的原因
静态内部类
除了双检锁实现单例的懒加载线程安全实现,静态内部类也可以做到,并且方法更简单。内部类只有被调用使用的时候才会被类加载器加载
class SingleExample04 {
private static class SingleExampleHolder{
//静态初始化器,由JVM来保证线程安全
private static final SingleExample04 singleExample = new SingleExample04();
}
public static SingleExample04 getInstance() {
return SingleExampleHolder.singleExample;
}
}
枚举单例模式
enum SingleExample05{
SINGLE_EXAMPLE_05;
}
是不是非常简单,非常简洁,而且无偿地提供了序列化机制。
TODO: 至于枚举的工作原理,目前我也不是很了解,等之后了解了再出一篇文章
注意点
- 构造方法私有化
- 自我实例化
- 提供一个公开方法让外部获取类的单例实例
如何破坏单例呢
其实这个问题可以换个问法
除了用new关键字创建对象,还有哪些方法可以创建对象
- 反射
- 序列化
- 克隆
具体如何实现,大家可以自行研究下 ^ v ^