单例模式的分类
- 饿汉式
- 静态常量
- 静态代码块
- 懒汉式
- 线程不安全
- 线程安全,同步方法
- 线程安全,同步代码块
- 双重检查锁
- 静态内部类
- 枚举
饿汉式
饿汉式,单例模式的一种类型,对于这个名字可以假想成:
有一天小明买了菜回到家,由于他特别饿,于是就把所有菜都用掉做了满满一桌子菜,而直到最后吃饱,仍然有一些菜从来没尝过,而且由于做的菜太多导致的燃气也用完了。
这里的菜就是我们要使用的对象,而小明就是单例类,燃气就是系统内存。在调用方准备使用对象前,就把所有的对象都实例化好,以供随时调用,但如果实例化工作量过大可能导致内存浪费
饿汉式-静态常量(⭐慎用)
这是最简单的单例模式,主要有以下几点核心思路
- 私有构造方法
- 私有静态常量,类加载时初始化常量对象
- 公有对象获取方法
示例代码如下
1 | public class SingletonType01 { |
示例代码本机执行结果:
1 | instance1 == instance2 true |
主方法中对于两次获取到的对象进行了对比,可以看到两者为同一对象,且hashcode相同
优点:
- 写法简单,在类装载的时候完成实例化,避免线程同步问题
缺点:
- 在类装载时就实例化,那可能这个对象从始至终都没有被用到,无形中造成资源浪费,没有懒加载效果
这种单例模式,可以使用,并且无需考虑多线程问题,但是存在内存浪费问题
饿汉式-静态代码块(⭐慎用)
饿汉式静态代码块的实现与静态常量基本类似,唯一不同就是对象的实例化从静态变量转移到了静态代码块中,但其都是在类加载是执行的
代码如下
1 |
|
示例代码本机执行结果:
1 | instance1 == instance2 : true |
可以看出同样是单例对象的效果,所有与饿汉式静态常量写法相比较,其优缺点也一样,都会造成内存浪费
懒汉式
前面提到的两种单例模式都是饿汉式,即无论用不用这个对象,他对会被实例化。
这里要提到的是另一种单例模式-懒汉式,即对象只有在需要使用的时候才进行实例化,同样可以想象成一个小场景
有一天小李特别饿,但是他很懒,不想做饭就到餐馆吃饭,看了菜单从里面选择点了一份牛肉拉面,后厨师傅马上给他做好,小李吃饱后就开心的回家了
虽然描述的比较抽象,小李是是对象使用方,菜单上的每一个菜是一个单例类,后厨师傅是JVM。
当你选定一个对象了之后才会为你立即创建,而不是提前把所有的对象都实例化好。这样实现了懒加载的效果
懒汉式-线程不安全(👎👎👎不可使用)
懒汉式的简易版本,这一实现方式虽然做到了懒加载,但是存在线程安全问题
示例代码如下:
1 |
|
示例代码本机执行结果:
1 | instance1 == instance2 : true |
简单的执行测试结果看似乎并无问题,做到了延迟加载(懒加载),并且实现了单例模式
但是!!!这一切都是单线程的前提下,一旦为多线程环境,在getInstance方法中会有严重的线程安全问题
分析:
- 假设有两个线程A、B
- A线程先到,判断INSTANCE为空,进入if内,准备进行对象初始化
- 此时B线程也到达if判断,发现INSTANCE仍为空(A还未完成对象实例化),B也进入if内。
这种情况下,待A、B执行完后,得到的将是两个对象。这就完全违背了单例模式的初衷!!
所以通常情况下,不推荐使用这种懒汉式的单例模式。因为绝大多数的应用场景都为多线程环境。
而在多线程环境下,这种实现方式完全不算单例模式的范畴,因为它会产生多个对象实例
懒汉式 - 同步方法(👎不推荐)
针对于线程不安全问题,对应则有线程安全的解决方案
即在getInstance方法上加入synchronized关键字,将其改造成同步方法,解决在多线程环境下的线程不安全问题
示例代码:
1 | class Singleton04 { |
如上,虽然解决了线程不安全问题,但是随之而来的是效率问题
分析:
- 每次调用getInstance方法都需要进行线程同步
- 实际上造成多个对象被实例化的仅仅只是方法中代码片段
所以总的来说,虽然解决的线程安全问题,但是由于效率不加,且有优化方案,故此种方式也不建议使用
针对同步方法带来的效率问题,有改进方案,但有一种错误的改进方案这里有必要提一下
将同步方法改造为同步代码块
,尝试减少同步的代码,来提高效率,示例代码如下:
1 | class Singleton04ErrorSolution { |
如上代码的本意是将同步方法细化到同步代码块,来进行效率优化,但是这样的改动起到了相反的效果
分析:
- 对实例化对象的代码片段进行同步,假设A、B两线程执行getInstance方法
- A线程判断INSTANCE为空后进入if内,准备执行同步代码块,此时B线程也判断INSTANCE为空,也进入了if内部,等待A线程执行完毕
- A线程执行完同步代码块后,实例化了一个对象,此时B线程开始执行,也创建了一个对象
从上面的分析可以看出,这种改进方案,属于想法正确,但是操作错误,导致不但没有解决效率问题,同时造成线程安全问题,是一定要避免的错误!!
懒汉式-同步代码块(👎不推荐)
基于上文提到的优化思路:将同步方法细化到同步代码块,那正确的改进方案可能会有下面这种写法:
1 |
|
从getInstance方法可以看到,使用了同步代码块的方式,并且同步的是if判断和实例化部分的代码
虽然达到了线程安全,但是基本上和同步方法的效率没什么区别,依旧每个线程进来后,都需要等待执行同步代码块。
这种方案只是为了和上面的错误同步代码块方式进行对比。真实业务中也不推荐使用这种方式!!!
双重检查锁(👍推荐使用)
想要实现懒加载,同时保证线程安全,同时提高效率。那么一起来看看双重检查锁的实现方式:
1 | class Singleton06 { |
首先先来看看该方案于前几种的不同点
- 使用synchronized关键字实现同步代码块
- 同步前同步后两次判断
- 使用了volatile关键字
分析
在getInstance方法中使用了Double-Check概念,配合同步代码块,保证线程安全。简单分析下其流程
- A、B、C 3个线程执行getInstance方法
- A、B线程都通过了第一个if判断,A线程抢到了锁,开始执行同步代码块中的逻辑,B等待
- A通过了第二个if判断,进行了INSTANCE的实例化操作,A完成操作,释放锁
- B开始执行同步代码块内容,B未通过第二个if(此时的INSTANCE不为空),直接返回INSTANCE对象,B释放锁
- 此时C开始执行getInstance方法,C未通过第一个if,直接返回INSTANCE对象
从上面分析过程中可以看到,无论有多少个线程,实例化代码只会被执行一次,意味着只会创建一个对象。
volatile
但是在整个流程中有一个小小的隐患
INSTANCE = new Singleton06();
它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:
①第一步:给 INSTANCE 分配内存空间;
②第二步:调用 Singleton06 的构造函数等,来初始化 INSTANCE;
③第三步:将 Singleton06 对象指向分配的内存空间(执行完这步 INSTANCE 就不是 null 了)。
这里的理想执行顺序是 1->2->3,实际在Jvm中执行顺序有可能是1->3->2,也有可能是 1->2->3。
这种现象被称作指令重排
也就是说第 2 步和第 3 步的顺序是不能保证的,这就导致了隐患的产生。
在线程A执行INSTANCE = new Singleton06();
是,JVM中的执行顺序是1->3->2,先进行分配内存再初始化INSTANCE,若在刚完成内存分配时,线程C开始执行第一个if判断,发现INSTANCE不为空,直接返回INSTANCE对象,此时的INSTANCE明显会出现问题。
在Java内存模型中,volatile 关键字作用可以是保证可见性且禁止指令重排。从而避免由于指令重排导致的异常隐患。
关于 volatile关键字和指令重排相关
可以参考此处
总结
双重检测锁的单例实现方案,可以实现延迟加载,同时线程安全并且效率高,在实际场景中是推荐使用的!
静态内部类(👍推荐使用)
除了双重检查锁被推荐使用外,静态内部类实现单例模式也是被推荐使用的一种
示例代码如下:
1 |
|
分析
- 该方案采用了类装载机制来保证
初始化实例时
只有一个线程,从而保证了线程安全 - 单例类Singleton07被装载时,静态内部类SingletonInstance是不会实例化的,只有调用getInstance方法时才会触发静态内部类SingletonInstance的装载,从而执行实例化代码
- 并且静态内部类的静态属性只会在第一次加载类的时候被初始化,所以做到了懒加载
结论
保证了线程安全,使用静态内部类的特点实现懒加载,并且有较高效率,推荐使用
枚举(👍推荐使用)
那么这么多的实现方案,Java中有没有一个公认的最佳枚举实现方案呢,当然有啊,通过枚举来实现
代码如下:
1 | public class SingletonType08 { |
如上代码是通过枚举来实现单例对象的创建
enum有且仅有private的构造器,防止外部的额外构造,这恰好和单例模式吻合,也为保证单例性做了一个铺垫。
枚举类型是线程安全的,并且只会装载一次。
枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,它保证线程安全,并防止外部反序列化的破坏。