编辑
2020-03-18
计算机基础科学
00
请注意,本文编写于 1285 天前,最后修改于 105 天前,其中某些信息可能已经过时。

目录

饿汉式-线程安全
饿汉式-线程安全
懒汉式-线程不安全
懒汉式-方法加锁-线程安全
懒汉式-双重检查-线程安全
懒汉式-静态内部类-线程安全(推荐)
无法破解的单例模式
DoubleCheck的隐患
单例模式的总结

单例模式属于创建型设计模式,一个类在虚拟机中只有一份实例。实现单例模式的核心思想在于构造函数私有化,主要实现方式分为两种:懒汉式和饿汉式。

mark

饿汉式-线程安全

java
//饿汉式单例 public class HungryTypeSingleByFinal { private static final HungryTypeSingleByFinal hungryTypeSingle = new HungryTypeSingleByFinal(); public static HungryTypeSingleByFinal getInstance(){ return hungryTypeSingle; } private HungryTypeSingleByFinal(){} }

饿汉式-线程安全

与上面一样,都是在类加载的时候就完成了初始化

java
//饿汉式单例 public class HungryTypeSingleByStaticBlock { private static HungryTypeSingleByStaticBlock hungryTypeSingleByStaticBlock; static { hungryTypeSingleByStaticBlock = new HungryTypeSingleByStaticBlock(); } private HungryTypeSingleByStaticBlock(){} public static HungryTypeSingleByStaticBlock getInstance(){ return hungryTypeSingleByStaticBlock; } }

懒汉式-线程不安全

java
//懒汉式单例(多线程下不安全) public class LazyTypeSingleNoSafe { private static LazyTypeSingleNoSafe lazyTypeSingleNoSafe; public static LazyTypeSingleNoSafe getInstance(){ if(lazyTypeSingleNoSafe == null){ lazyTypeSingleNoSafe = new LazyTypeSingleNoSafe(); } return lazyTypeSingleNoSafe; } private LazyTypeSingleNoSafe(){} }

懒汉式-方法加锁-线程安全

java
//懒汉式单例 + 获取对象加锁 public class LazyTypeSingleSafe { private static LazyTypeSingleSafe lazyTypeSingleSafe; public static synchronized LazyTypeSingleSafe getInstance(){ if(lazyTypeSingleSafe == null){ lazyTypeSingleSafe = new LazyTypeSingleSafe(); } return lazyTypeSingleSafe; } private LazyTypeSingleSafe(){} }

懒汉式-双重检查-线程安全

java
//懒汉式单例 + 双重锁检查 public class LazyTypeSingleSafeDoubleCheck { private static LazyTypeSingleSafeDoubleCheck lazyTypeSingleSafeDoubleCheck; public static LazyTypeSingleSafeDoubleCheck getInstance(){ if(lazyTypeSingleSafeDoubleCheck == null){ synchronized (LazyTypeSingleSafeDoubleCheck.class){ if(lazyTypeSingleSafeDoubleCheck == null){ lazyTypeSingleSafeDoubleCheck = new LazyTypeSingleSafeDoubleCheck(); } } } return lazyTypeSingleSafeDoubleCheck; } private LazyTypeSingleSafeDoubleCheck(){} }

这种做法相对于上面的做法的好处就是,如果已经实例化了则直接返回对象,而不是像上面那样每次都进入同步方法,双重检查只是在对象未初始化的时候加锁,一旦对象已经初始化则后面的线程无需加锁直接获取到了单例对象,无疑减小了开销。

表面看起来线程安全,逻辑也没问题,实则有漏洞,后面会讲

懒汉式-静态内部类-线程安全(推荐)

java
//懒汉式单例 静态内部类实现(推荐) public class LazyTypeSingleSafeInnerClass { private static class LazyTypeSingleSafeInnerClassHolder{ private static LazyTypeSingleSafeInnerClass singleSafe = new LazyTypeSingleSafeInnerClass(); } public static LazyTypeSingleSafeInnerClass getInstance(){ return LazyTypeSingleSafeInnerClassHolder.singleSafe; } private LazyTypeSingleSafeInnerClass(){} }

这种方式是比较推荐的方式,从外部无法访问静态内部类LazyTypeSingleSafeInnerClassHolder,只有当调用LazyTypeSingleSafeInnerClass.getInstance方法的时候,才能得到单例对象singleSafe。 这里要注意的是singleSafe对象初始化的时机并不是在单例类LazyTypeSingleSafeInnerClass被加载的时候,而是在调用getInstance方法,使得静态内部类LazyTypeSingleSafeInnerClassHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。

无法破解的单例模式

上述单例模式都可以通过反射的方式构造出新的对象,毕竟反射大法香呀:

java
public static void testLazyTypeSingleSafeInnerClass() throws Exception { System.out.println(LazyTypeSingleSafeInnerClass.getInstance() == LazyTypeSingleSafeInnerClass.getInstance()); //true Class<LazyTypeSingleSafeInnerClass> innerClassClass = LazyTypeSingleSafeInnerClass.class; Constructor<LazyTypeSingleSafeInnerClass> constructor = innerClassClass.getDeclaredConstructor(null); constructor.setAccessible(true); System.out.println(constructor.newInstance(null) == constructor.newInstance(null)); //false } public static void threadEnvTest() throws InterruptedException { Set<LazyTypeSingleNoSafe> typeSingleNoSafeList = new HashSet<>(); AtomicInteger atomicInteger = new AtomicInteger(0); while(atomicInteger.getAndSet(atomicInteger.intValue() + 1) < 50){ new Thread(()->{ LazyTypeSingleNoSafe instance = LazyTypeSingleNoSafe.getInstance(); typeSingleNoSafeList.add(instance); }).start(); } for(LazyTypeSingleNoSafe lazyTypeSingleNoSafe: typeSingleNoSafeList){ System.out.println(lazyTypeSingleNoSafe); } //single.LazyTypeSingleNoSafe@6e5802d8 //single.LazyTypeSingleNoSafe@6706a70b //single.LazyTypeSingleNoSafe@51a22f1d }

通过上述例子我们也看到了,在反射眼里,一切都是弟弟,所以我们根本不可能造出真正的单例,但是我们却可以通过枚举这个特性来实现绝对的单例模式和多例模式!

java
//绝对的单例模式(之前的通过反射都可以破解) public enum AbsoluteSingleSafe { ONLY_ONE_SINGLE }

我们破解一下枚举试试:

java
public static void testAbsoluteSingleSafe() throws Exception { System.out.println(AbsoluteSingleSafe.ONLY_ONE_SINGLE == AbsoluteSingleSafe.ONLY_ONE_SINGLE); //true Class<AbsoluteSingleSafe> absoluteSingleSafeClass = AbsoluteSingleSafe.class; Constructor<AbsoluteSingleSafe> constructor = absoluteSingleSafeClass.getDeclaredConstructor(null); constructor.setAccessible(true); System.out.println(constructor.newInstance(null) == constructor.newInstance(null)); //Exception in thread "main" java.lang.NoSuchMethodException: single.AbsoluteSingleSafe.<init>() }

DoubleCheck的隐患

我们回顾一下DoubleCheck的代码:

java
public class LazyTypeSingleSafeDoubleCheck { private static LazyTypeSingleSafeDoubleCheck lazyTypeSingleSafeDoubleCheck; public static LazyTypeSingleSafeDoubleCheck getInstance(){ if(lazyTypeSingleSafeDoubleCheck == null){ synchronized (LazyTypeSingleSafeDoubleCheck.class){ if(lazyTypeSingleSafeDoubleCheck == null){ lazyTypeSingleSafeDoubleCheck = new LazyTypeSingleSafeDoubleCheck(); } } } return lazyTypeSingleSafeDoubleCheck; } private LazyTypeSingleSafeDoubleCheck(){} }

问题出在哪里呢?

我们可以假设这样的情况,当两个线程一先一后访问getInstance方法的时候,当A线程正在构建对象,B线程刚刚进入方法:

mark

这种情况表面看似没什么问题,要么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对象。如下图所示:

mark

由于线程A还未完成初始化工作,但是线程B检测到对象已经不为空,于是最终返回的是空对象!!那么应该如何避免呢?其实只需要在instance对象前面增加一个修饰符volatile就好了,关于可以看《重新认识volatile》 这篇文章,里面讲述的比较详细,在此不再赘述。所以完整的双重检查的代码是:

java
//懒汉式单例 + 双重锁检查 public class LazyTypeSingleSafeDoubleCheck { private volatile static LazyTypeSingleSafeDoubleCheck lazyTypeSingleSafeDoubleCheck; public static LazyTypeSingleSafeDoubleCheck getInstance(){ if(lazyTypeSingleSafeDoubleCheck == null){ synchronized (LazyTypeSingleSafeDoubleCheck.class){ if(lazyTypeSingleSafeDoubleCheck == null){ lazyTypeSingleSafeDoubleCheck = new LazyTypeSingleSafeDoubleCheck(); } } } return lazyTypeSingleSafeDoubleCheck; } private LazyTypeSingleSafeDoubleCheck(){} }

单例模式的总结

所以如果想要实现线程安全的单例模式,可以使用饿汉式、DoubleCheck(加volatile的版本),静态内部类,枚举等方式;想要使用懒加载策略就不能使用枚举了,只能DoubleCheck(加volatile的版本),静态内部类;如果想实现反射也无法破解的单例那么只能用枚举了,但是一般情况下不会去刻意排斥反射。所以比较推荐的方案还是静态内部类,简单实用而且线程安全。

本文作者:Tim

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!