单例模式是常见的设计模式之一,虽说现在开发框架都帮我们实现了单例模式,但是单例模式的思想和原理还是要去了解的(面试太常问了),今天让我们一起来搞透单例模式。
一. 什么是单例模式?
-
单例模式就是在内存中只创建一次对象的模式,如果我们在应用中多次使用同一个对象且作用相同时,就应该考虑用单例模式,因为多次创建对象肯定造成内存资源浪费和吞吐量下降,单例模式可以防止空间和创建时造成的时间浪费。
二. 单例模式的几种实现
-
首先需要明确一点,单例实现的精髓就是私有化构造方法。
懒汉式:
class Single{
private static Single object;
// 私有化构造方法
private Single(){};
public static Single getSingle() {
// bean为null才去实例化,否则直接返回
if(object == null) {
object = new Single();
}
return object;
}
}
-
这种方式是利用一个懒加载的思想,需要Single的时候才会去实例化Single,优点是节省空间,但是在多线程下getSingle方法并不是线程安全的,这个后面再做分析。
饿汉式:
class Single{
// 静态变量赋值
private static Single object = new Single();
// 私有化构造方法
private Single(){};
// 获取bean方法直接返回变量
public static Single getSingle() {
return object;
}
}
-
这种方式是通过给静态变量赋值的方式让Single类编译时被加载,getSingle方法内只有一行,所以是线程安全的,但是因为编译时就加载Single进内存(也可以理解为程序启动时就加载对象进内存),资源浪费也是饿汉式的一个缺点。
下面我们来解析下懒汉式为什么会有线程安全的问题:
图1
-
如图1,多线程情况下,在时刻T,线程A和线程B都判断single为null,从而进入if代码块中都执行了new Single()的操作创建了两个对象,就和我们当初的单例初衷相悖而行。
如何解决线程安全问题呢?下面我们来逐步分析。
在getSingle()方法上加synchronized?
-
这个方式固然可以解决上面出现的线程安全问题,但是有两个问题:一是每次获取Single对象都要去获取锁,本身申请锁就是一个耗时的行为;二是锁的粒度是整个方法,一般我们在开发中都不会去对整个方法进行加锁,会去尽量减小锁的粒度从而提升代码的性能。
下面我们就从这两个问题去优化懒汉式的getSingle()方法:
public static Single getSingle() {
// 第一次检查防止每次获取bean都加锁
if(object == null) {
synchronized(Single.class) {
// 第二次检查防止第一次创建bean时并发线程多次创建对象
if(object == null) {
object = new Single();
}
}
}
return object;
}
-
我们将同步方法改成的同步代码块,并缩小了锁的粒度,在获取锁之前我们先做一次bean的检查,如果不为空直接返回bean,为空就去争抢锁,抢到锁的线程走到同步代码块中再检查一次bean是否为空(这一次检查是为了防止代码块外部阻塞的线程进来再次创建bean),发现为空就去创建对象,释放锁并返回对象,这个时候在同步代码块外阻塞的线程争抢到锁代码块内部,第二次检查发现bean不为空就直接返回。
这样既保证了线程安全又保证了性能。
但是是不是现在这种方式就真的没有问题了呢?
创建一个对象,在JVM中会经过三步:
(1)为object对象分配内存空间
(2)初始化object对象
(3)将object指向分配好的内存空间
在JVM中是会发生指令重排优化的。
-
指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能。
-
打个比方:我是一名学生,我今晚回家的任务是写作业和复习白天的功课,然后我发现作业很难有很多不会的,我就先去复习了一下功课然后再去写作业,这就类似JVM指令重排。
那指令重排会造成什么问题?
-
在多线程场景下有可能先执行了(1)、(3)步骤,但是并没有真正初始化对象,此时别的线程去获取bean发现不为空直接返回了,但是真正使用时就会出现空指针异常。
那有什么办法可以防止指令重排呢?
-
使用volatile关键字,volatile关键字有防止指令重排和线程立即可见作用,在java JUC包下的类中非常常见。
那么我们最终版本单例模式就出炉了!
市面上也有个高大尚的名字:双重校验加锁模式
class Single{
// volatile关键字防止指令重排造成空指针异常
private static volatile Single object;
// 私有化构造方法
private Single(){};
public static Single getSingle() {
// 第一次检查防止每次获取bean都加锁
if(object == null) {
synchronized(Single.class) {
// 第二次检查防止第一次创建bean时并发线程多次创建对象
if(object == null) {
object = new Single();
}
}
}
return object;
}
}
-
Spring框架管理的bean也是用了双重校验加锁的方式,唯一的区别是bean没有加volatile,原因也很显而易见:因为spring在容器启动前将所有的bean加载好,不存在立刻获取的情况,所以不会出现上述所说的并发空指针问题。
三. 破坏单例的方式以及如何防止破坏单例?
那么我们单例模式写的那么完美是不是真的能保证单例呢?有没有什么手段可以破坏上面写的单例模式呢?
破环单例的几种方式:
首先从创建对象的几种方式去分析:new,clone,反射,反序列化。
1. new肯定不行,构造器已经私有化。
2. clone也不行,没有实现cloneable接口的话也不行。
3. 反射:暴力反射获取构造器newInstance,实例化新的数据。
防止暴力获取:在私有无参构造里判断下实例是否为空,不为空直接抛出异常阻止构造器初始化对象。
private Singleton(){
if (singleton != null) {
throw new RuntimeException();
}
}
4. 反序列化也可以。
// 序列化反、序列化代码示例
// 序列化
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("single"));
objectOutputStream.writeObject(singleton);
// 反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("single")));
Singleton newSingleton = (Singleton) objectInputStream.readObject();
我们可以看一下源码readObject()做了哪些事情:
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
// 这步做了反序列化的操作
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
我们进readObject0方法看下,发现代码走到TC_OBJECT的case里
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
继续点进readOrdinaryObject方法
// 删去部分源码
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
// 这里去判断bean里是否有readResolve这个方法
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
// 如果有就调用readResolve() 并将返回值赋给反序列化的bean
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
-
第八行源码我们发现反序列化在进行反序列化之前会走到hasReadResolveMethod方法里判断当前类是否含有readResolve方法,有的话直接将变量返回,这样我们就能防止反序列化破坏单例了(readResolve这个方法在很多工具类和日期类中都有,就是防止反序列化破坏单例bean)。
由此我们做一个操作:
我们在需要实现单例的bean里加上readResolve方法
private Object readResolve() {
return singleton;
}
总结一下:
-
1. 防止反序列化破坏单例:进行反序列化的底层方法是readObject0(),里面进行反序列化之前会进行判断,判断单例类里是否有Object readResolve() 方法,如果有将方法内的返回值返回,我们"重写"(不算真正意义的重写)readResolve方法,将单例直接返回。
-
2. 防止暴力反射破坏单例:在私有无参构造里判断下实例是否为空,不为空直接跑出异常阻止构造器初始化对象。
最终我们单例模式的最终版本出来了:
public class Singleton implements Serializable {
private static volatile Singleton singleton;
private Singleton(){
if (singleton != null) {
throw new RuntimeException();
}
}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
private Object readResolve() {
return singleton;
}
}
到这里你会不会觉得实现单例bean有点麻烦?
《effective java》的作者在书中说“单元素的枚举类型已经成为实现Singleton的最佳方法”;
枚举天然单例和线程安全,暴力反射和反序列化也会报错,实现方式也很简单:
public enum SingleEnum {
SINGLE_ENUM;
public SingleEnum getSingleEnum() {
return SINGLE_ENUM;
}
}
就和正常的bean一样去定义区别是类型为enum。
到这里,我们今天单例模式的探讨就要结束了,你实现单例会去使用枚举吗?
大家可以关注下我的公众号“阿东编程之路”,里面全是干货!