单例模式属于创建型设计模式,一个类在虚拟机中只有一份实例。实现单例模式的核心思想在于构造函数私有化,主要实现方式分为两种:懒汉式和饿汉式。
饿汉式-线程安全
1 | //饿汉式单例 |
饿汉式-线程安全
与上面一样,都是在类加载的时候就完成了初始化
1 | //饿汉式单例 |
懒汉式-线程不安全
1 | //懒汉式单例(多线程下不安全) |
懒汉式-方法加锁-线程安全
1 | //懒汉式单例 + 获取对象加锁 |
懒汉式-双重检查-线程安全
1 | //懒汉式单例 + 双重锁检查 |
这种做法相对于上面的做法的好处就是,如果已经实例化了则直接返回对象,而不是像上面那样每次都进入同步方法,双重检查只是在对象未初始化的时候加锁,一旦对象已经初始化则后面的线程无需加锁直接获取到了单例对象,无疑减小了开销。
表面看起来线程安全,逻辑也没问题,实则有漏洞,后面会讲
懒汉式-静态内部类-线程安全(推荐)
1 | //懒汉式单例 静态内部类实现(推荐) |
这种方式是比较推荐的方式,从外部无法访问静态内部类LazyTypeSingleSafeInnerClassHolder,只有当调用LazyTypeSingleSafeInnerClass.getInstance方法的时候,才能得到单例对象singleSafe。 这里要注意的是singleSafe对象初始化的时机并不是在单例类LazyTypeSingleSafeInnerClass被加载的时候,而是在调用getInstance方法,使得静态内部类LazyTypeSingleSafeInnerClassHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。
无法破解的单例模式
上述单例模式都可以通过反射的方式构造出新的对象,毕竟反射大法香呀:
1 | public static void testLazyTypeSingleSafeInnerClass() throws Exception { |
通过上述例子我们也看到了,在反射眼里,一切都是弟弟,所以我们根本不可能造出真正的单例,但是我们却可以通过枚举这个特性来实现绝对的单例模式和多例模式!
1 | //绝对的单例模式(之前的通过反射都可以破解) |
我们破解一下枚举试试:
1 | public static void testAbsoluteSingleSafe() throws Exception { |
DoubleCheck的隐患
我们回顾一下DoubleCheck的代码:
1 | public class LazyTypeSingleSafeDoubleCheck { |
问题出在哪里呢?
我们可以假设这样的情况,当两个线程一先一后访问getInstance方法的时候,当A线程正在构建对象,B线程刚刚进入方法:
这种情况表面看似没什么问题,要么Instance还没被线程A构建,线程B执行if(lazyTypeSingleSafeDoubleCheck== null)的时候得到true;要么Instance已经被线程A构建完成,线程B执行 if(lazyTypeSingleSafeDoubleCheck== null)的时候得到false。真是如此吗?答案是否定的。这里涉及到了JVM编译器的指令重排。
一句简单的lazyTypeSingleSafeDoubleCheck = new LazyTypeSingleSafeDoubleCheck();
会被编译器编译成如下JVM指令 :
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
当线程A执行完1和3时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。如下图所示:
由于线程A还未完成初始化工作,但是线程B检测到对象已经不为空,于是最终返回的是空对象!!那么应该如何避免呢?其实只需要在instance对象前面增加一个修饰符volatile就好了,关于可以看《重新认识volatile》 这篇文章,里面讲述的比较详细,在此不再赘述。所以完整的双重检查的代码是:
1 | //懒汉式单例 + 双重锁检查 |
单例模式的总结
所以如果想要实现线程安全的单例模式,可以使用饿汉式、DoubleCheck(加volatile的版本),静态内部类,枚举等方式;想要使用懒加载策略就不能使用枚举了,只能DoubleCheck(加volatile的版本),静态内部类;如果想实现反射也无法破解的单例那么只能用枚举了,但是一般情况下不会去刻意排斥反射。所以比较推荐的方案还是静态内部类,简单实用而且线程安全。
- 本文作者: Tim
- 本文链接: https://zouchanglin.cn/2020/03/18/818927595.html
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 许可协议。转载请注明出处!