第三梦 单例模式
初识单例
单例模式,算是我们代码中经常遇见的设计模式之一了。当然我们也上手很快,但是其中的坑也不少,不好好研究一下,这些坑还真不好跳过去。单例简单分分别为懒汉模式、饿汉模式,那我们就从懒汉模式开始吧。
懒汉模式(线程非安全)
这里定义一个私有的全局变量singletonPattern,然后通过一个公有的静态方法对singletonPattern进行判空,如果为空,就new一个类对象出来,然后返回该对象。该种方式可以实现类对象在使用的时候才创建,也就是延时加载。
- 1 public class SingletonPattern {
- 2
- 3 private static SingletonPattern singletonPattern = null;
- 4
- 5 private SingletonPattern() {
- 6 }
- 7
- 8 public static SingletonPattern getInstance(){
- 9 // if这里存在竞态条件
- 10 if(singletonPattern == null){
- 11 singletonPattern = new SingletonPattern();
- 12 }
- 13 return singletonPattern;
- 14 }
- 15 }
懒汉模式(线程安全、低效)
一种比较简单的方式,是同步获取实例化的方法getInstance(),也就是加上synchronized关键字。当然这种方式是非常低效的(jdk后面的版本对synchronized关键字段的底层代码做了很强的优化,所以也不是不可以考虑),具体如下:
- 1 public class SingletonPattern {
- 2
- 3 private static SingletonPattern singletonPattern = null;
- 4
- 5 private SingletonPattern() {
- 6 }
- 7
- 8 public static synchronized SingletonPattern getInstance(){
- 9 if(singletonPattern == null){
- 10 singletonPattern = new SingletonPattern();
- 11 }
- 12 return singletonPattern;
- 13 }
- 14 }
懒汉模式(线程安全、高效)
1、双重锁校验DCL(半成品,问题代码,面试考点),注意看下面罗列的四步。
- 1 public class SingletonPatternDcl {
- 2
- 3 private static SingletonPatternDcl singletonPatternDcl = null;
- 4
- 5 private SingletonPatternDcl() {
- 6 }
- 7
- 8 public static SingletonPatternDcl getInstance(){
- 9 if(singletonPatternDcl == null){ //1、在实例化的情况下,不需要执行加锁动作,性能提高
- 10 synchronized (SingletonPatternDcl.class){ //2、对类上锁,多个线程的情况下,只有一个线程能够创建对象
- 11 if(singletonPatternDcl == null){ //3、实例化对象为空的情况下创建对象
- 12 singletonPatternDcl = new SingletonPatternDcl(); //4、创建对象
- 13 }
- 14 }
- 15 }
- 16 return singletonPatternDcl;
- 17 }
- 18 }
2、完美的DCL。上面的DCl看起来是非常完美的,所有的逻辑都考虑到了,但是上面的第四步singletonPatternDcl = new SingletonPatternDcl()创建对象的过程其实并非是一个原子操作,这就导致了问题的产生。我们来分析一下第四步在JVM中具体做了哪些事情:
- a、给singletonPatternDcl分配内存空间
- b、调用SingletonPatternDcl的构造函数来初始化该成员变量
- c、将singletonPatternDcl对象指向a步骤分配的内存空间(这一步执行完之后,singletonPatternDcl就为非null了)
而在JVM的即时编译器中存在指令重排序的优化,如果c步骤在b步骤之前执行的话:b执行了,singletonPatternDcl不为空了,第二个线程来了,发现singletonPatternDcl已经不为null了,然后直接返回。但是其实这个时候singletonPatternDcl只是一个内存地址,根本还没有初始化,程序就理所当然的报错了。解决的方法很简单,基于volatile解决方案,如下所示:
private static volatile SingletonPatternDcl singletonPatternDcl = null;
volatile的特性禁止指令重排序,保证了上述a、b、c一定会按着abc的顺序执行,也就避免了上述产生问题的场景。
饿汉模式(天然的线程安全)
利用类加载的机制,我们可以在类一开始加载的时候就初始化一个实例对象。缺点是无法实现懒加载,并且在某些需要使用动态参数的情况下无法使用。
- 1 public class SingletonPatternSafe {
- 2
- 3 private static SingletonPatternSafe singletonPatternSafe = new SingletonPatternSafe();
- 4
- 5 private SingletonPatternSafe() {
- 6 }
- 7
- 8 public SingletonPatternSafe getInstance() {
- 9 return singletonPatternSafe;
- 10 }
- 11 }
这里加上final也是可以的
- private static final SingletonPatternSafe singletonPatternSafe = new SingletonPatternSafe();
静态内部类(天然的线程安全)
这种方式的单例实现,也是基于JVM本身机制保证了线程安全。其内部类Holder只有getInstance()方法可以访问。读取的实例的时候也不需要进行同步,没有性能的损失。
- 1 public class SingletonPatternHolder {
- 2
- 3 private static class Holder {
- 4 private static final SingletonPatternHolder INSTANCE = new SingletonPatternHolder();
- 5 }
- 6
- 7 private SingletonPatternHolder() {
- 8 }
- 9
- 10 public static SingletonPatternHolder getInstance(){
- 11 return Holder.INSTANCE; //懒汉式的,只有访问getInstance()方法的时候才实例化
- 12 }
- 13
- 14 }
枚举方式(绝对的线程安全)
枚举实现单例模式有三个特性:自由序列化、线程安全、保证单例。
- enum的实现是通过继承了Enum类来实现的,enum结构不能作为子类来继承其他类,但是可以用来实现接口类;
- 由于enum内部的实现方式其实是final类型的,所以enum类不可以被继承;
- enum有且仅有private构造器,防止外部的额外构造,这恰好和单例模式相符合;
- 其内部也是枚举量未被初始化,之后会在静态代码中进行初始化,这就非常类似饿汉模式;
- 对于序列化和反序列化,因为每一个枚举类型和枚举变量在JVM中都是唯一的,所以Java在序列化和反序列化枚举时做了特殊规定,枚举的writeObject、readObject、readReplace和readResolve等方式是被编译器禁止的,因此不存在实现序列化接口之后调用readObject会重新创建的心得对象从而破坏单例的问题。
基于上述描述,我们发现enum的方式来构造单例模式,代码实现起来非常的简单、自由序列化。并且也是线程的安全,相比起来应该更优选择
- 1 public enum SingletonPatternEnum {
- 2
- 3 /**
- 4 * 实例化对象
- 5 */
- 6 INATANCE
- 7 }
代码实例
我的代码放在GitHub,小伙伴可以作为一个参考、
参考博文
- SingletonPattern、我们平常用到的一个设计模式,有必要深入学习,掌握精髓,在实战中灵活运用。感谢前辈们的分享做为引路人。-------书山有路、人儿需行<<