并发学习——单例模式的演进

287 阅读5分钟

这是我参与8月更文挑战的第6天,活动详情查看8月更文挑战

  本文分析主要分析了单例模式在演进过程的变化,同时总结了单例模式中常见的单例形式的写法, 希望能帮你更好的理清单例模式的迭代过程。

  一般来说在构建单例模式时主要有懒汉和饿汉两种形式,大致的演进过程基本如下所示:

image.png

由于饿汉式单例相对来说比较简单,不在此讨论 ,重点分析后几种方式的写法

单例模式的优势:

1.节省内存,节省资源

2.保证结果正确性,避免因资源竞争而导致的错误

比如对文件的写入,如果多个实例同时对文件内容进行读写,很容易导致不一致性问题。

懒汉式单例分析

所谓的懒汉式单例就是指当使用到该实例时在进行构建,这样可以保证构造出的对象会被使用到。避免了构造出来而不使用的问题。

懒汉式单例的基础版写法

    // 懒汉式单例
    public class LazySingleton {
        private static LazySingleton lazySingleton = null;

        public static LazySingleton getInstance(){
            if (lazySingleton == null){
                return new LazySingleton();
            }
            return lazySingleton;
        }
    }

这样写时存在线程安全问题的,接下来我们就开始来分析解决该问题

   通常来说,在书写懒汉式单例时如果不加同步块的限制时很容易到导致线程问题发生的,但加上同步关键字 synchronized的限制如果所加的问题不正确,也有可能导致问题。

synchronized若采用下图所示的修饰方式,依旧存在问题\color{red}若采用下图所示的修饰方式,依旧存在问题

public static  LazySingleton getInstance(){
    if (lazySingleton == null){
      synchronized (LazySingleton.class) {
          return new LazySingleton();
      }
    }
    return lazySingleton;
}

此时虽然加上了synchronized关键字,但是还面临这线程不安全的问题。假设此时有两个线程A,B同时都执行到 synchorinzed(LazySingleton.class)这段代码处.

由于加锁的原因,仅能有一个线程进入执行,此时假如A线程进入,构建出对象后,程序退出锁被释放,此时B便会获取锁信息,但此时B获得到资源后也会执行构建逻辑,构建出一个对象信息,此时变回构建两个不同的lazySingleton对象,此时本质上并非单例。

针对这种不安全的懒汉式单例,其实有新的改进方法,那便是双重检查锁机制

双重检查锁单例如下

// 双重检查锁机制下的单例模式
public class LazySingleton {
   private volatile static LazySingleton lazySingleton = null;

   public static  LazySingleton getInstance(){
       if (lazySingleton == null) {
           synchronized (LazySingleton.class) {
               if (lazySingleton == null) {
                   lazySingleton = new LazySingleton();
               }
           }
       }
       return lazySingleton;
   }
}

  其实去掉第一重也是可以的,但是此时相当于将方法变成单线程模式,带来的性能损耗增加。此时相当于为类加锁,保证了每次仅有一个线程可以进入被加锁的对象内部进行执行,带来性能问题,所以一般不推荐去掉。

延伸:标准的双重检查锁,一般都需要加入volatile关键字进行修饰最主要的原因在于:

  避免java底层的重排序所带来的问题,由于jvm底层会自动对java代码进行优化,而new创建对象这一操作并不是一个原子性操作,其存在指令的重排序。

一般而言实例化主要经过如下步骤 :

    1.分配instance对象空间\color{#00BFFF} 1.分配instance对象空间

   2.初始化singleton对象信息\color{#00BFFF} 2.初始化singleton对象信息

   3.将初始化的对象信息指向分配的内存空间,此时instace对象才不为null\color{#00BFFF} 3.将初始化的对象信息指向分配的内存空间,此时instace对象才不为null

   指令重排序则有肯能导致第三步优于第二步执行, 此时可能面临的问题则是有可能得到一个空对象,所以可以使用volatiled关键字来禁止jvm的指令重排序现象。所以双重检查锁机制下一般需要借助volatile关键字 。

   如果将synchorinzed加到获取单例对象的方法之上,那么此时完全是线程安全的,因为同一时刻仅能有一个线程进入到方法内部,如下图所示,这样的单例模式不存在 任何线程问题。具体写法如下所示:

public static synchronized  LazySingleton getInstance(){
   if (lazySingleton == null){
       return new LazySingleton();
   }
   return lazySingleton;
}

静态内部类单例

这种形式采用一种饿汉式的形式,但是同饿汉有有所不同,饿汉式只要类装载就会实例化,没有lazy-loading的效果。而静态内部类的形式只有当调用方法式,才会进行类的加载,从而完成加载的单例的创建。

具体写法如下

// 静态内部类形式的单例模式
public class StaticInnerClassSingleton {
    private static class InnerClass {
        private static StaticInnerClassSingleton
                staticInnerClassSingleton = new StaticInnerClassSingleton();
    }

    public StaticInnerClassSingleton getInstance() {
        return InnerClass.staticInnerClassSingleton;
    }
}

枚举单例

effective中有说:最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。

枚举式单例的写法则具体如下所示

image.png

小结

 单例模式可以说是设计模式中最常用到的一种,同时其迭代演进之路也是一个值得深究思考的地方,从一个普普通通的单例可以扩展至synchronized你对的理解,比如:

  • synchroized关键字的作用

  • synchroized使用方式

  • synchroized背后的原理

  • synchroized中偏向锁、轻量级锁、重量锁升级过程,升级细节

  • synchroized缺点及和 Lock的区别是什么

      通过一个简单的单例很容易就能扩展至并发相关的问题,或者说当你提及volatiled关键时,其背后原理、优势、实现原理是否能如数家珍?

  简单的事物中往往蕴含着不简单的东西,知识永无止境,愿你我都能时刻保持一颗学徒的心。加油 ~~