细说Java中的几种单例模式

在Java中,单例模式分为不少种,本人所了解的单例模式有如下几种,若有不全还请你们留言指点:java

  • 饿汉式
  • 懒汉式/Double check(双重检索)
  • 静态内部类
  • 枚举单例

1、饿汉式android

饿汉式是在jvm加载这个单例类的时候,就会初始化这个类中的实例,在使用单例中的实例时直接拿来使用就好,由于加载这个类的时候就已经完成初始化,而且因为是已经加载好的单例实例所以是线程安全的,并发获取的状况下不会有问题,是一种可投入使用的可靠单例。面试

优势:使用起来效率高、线程安全安全

缺点:因为jvm在加载单例类的时候须要初始化单例实例,所以在加载单例的时候针对jvm内存不够友好。多线程

2、懒汉式并发

最简单的懒汉式,核心思想就是弥补饿汉式的缺点,在jvm加载单例类的时候不去初始化实例,而是在第一次获取实例的时候再去初始化实例。可是这样理论完美的单例在使用的时候有一个致命的缺点,在多线程使用的状况下,有时会出现不一样线程从单例实例中获取不一样的实体。针对多线程环境中并不可靠。app

优势:针对jvm内存比较友好,实现了实例的懒加载。jvm

缺点:多线程环境下不安全,会出现不一样线程从单例实例中获取不一样的实体的状况。ide

具体为何会出现不一样线程从单例实例中获取不一样的实体的状况呢?以下图,咱们经过分析去解释,为什么他是线程不安全的。函数

假设,当前有两个线程同时首次获取此单例中的实例时:

  1. 线程一执行getInstence方法,并判断instance实例是否已经被初始化。
  2. 线程一判断instance为null,执行到 2 处,此时线程一尚未开始执行,而后执行权被线程二获取,线程一进入等待。
  3. 线程二执行到 1 处判断instance为null,由于线程一即将开始初始化instance,可是尚未初始化。
  4. 线程二执行到 2 处开始初始化instence方法,并完成初始化,返回一个instance实例。
  5. 这时线程一被唤醒,继续从 2 处执行,开始初始化instence方法,而且也返回一个instance实例。

这样,线程一和线程二从单例中获取了两个不一样的实例。针对懒汉式的这种线程不安全的现象,攻城狮们也是开始头脑风暴来改善它,比较容易想到的是将getInstence方法加锁,来实现懒汉式的线程安全:

这样虽然看似解决问题了,可是未免太过于激进了,synchronized锁住获取实例的整个方法,所以在并发获取单例实例的时候会有性能问题,而且线程安全问题的出现只是在第一次获取实例的状况才会出现,初始化以后不会再出现性能问题,synchronized锁的运用未免因小失大。

因而为了线程安全,还为了能在并发状况下高效的性能,便有了Double check(双重检索)的懒汉式单例

Double check的理论为:当第一次建立单例实例的时候,只有一个线程能够去建立实例,所以不会出现多个线程获取不一样实例的状况。

假设时间序列:

  1. 线程一进入getInstence方法
  2. 线程一判断instence为null,并在 1 处进入synchronize块,此时线程二开始执行,线程一等待
  3. 线程二进入getInstence方法,判断instence为null,并准备进入synchronize块,此时发现synchronize块的锁被占用,所以进入等待
  4. 线程一开始再次判断instence为null,而后开始初始化instence实例,而后释放synchronize的锁,获取到了实例执行完成
  5. 此时线程二开始获得synchronize锁,进入synchronize块再次判断instence是否为null,发现instence此时已经有值,释放锁,直接获取instence实例返回

Double check的理论看起来很是的完美,然而一切到头来发现仅仅是想得美而已,在实际运行中他仍是有问题的。

年轻稚嫩的猿也许会一脸懵逼,老谋深算的猿也许会微微一笑,可是可能他们都会想 弄啥子嘞?

其实,这个理论的失败,并非jvm实现的bug,而是归咎于Java平台的内存模型,Java的内存模型是围绕着如何在并发过程当中处理原子性、可见性、有序性这3个特征创建的,而针对有序性,引用深刻JVM虚拟机中的一句话解释是:若是在本线程中观察,全部的操做都是有序的;若是在一个线程中观察另外一个线程,全部的操做都是无序的。前半句是指“线程内表现为串行指令”,后半句是指“指令重排序”现象和“工做内存与主内存同步延迟”现象。而针对原子性,看似简单一行代码,通过虚拟机编译成字节码信息后,可能就不是一行代码了。而针对可见性,一个线程改变的变量值,并不会马上对其余线程可见。

而上面Double check代码失败的源头就是 instence = new DoubleCheck(); 这句话,而这句看似简的一句话,其实在虚拟机中分红了三个步骤:

  1. 为即将实例化的对象分配内存空间
  2. 初始化单例实体对象执行构造函数
  3. 将内存空间地址赋值给instence实例引用

也就是说其实咱们所谓的new对象 并非一个原子操做,而且,针对上面的2 3 步骤虚拟机会进行指令重排序,若是上面的Double check代码的对象实例化的通过重排序顺序变成1 3 2 的话,就会出现问题:

  1. 线程一进入getInstence方法
  2. 线程一判断instence为null,并在 1 处进入synchronize块
  3. 线程一再次判断instence为null,最后执行到 3 处,然而分配完内存,获取到实例地址,此时instence再也不为null,可是还未初始化对象执行构造方法,此时县城而获取执行权,线程一被挂起
  4. 线程二获取getInstence方法,并判断instence再也不null,而后获取到了一个instence对象的地址,可是此时instence对象并未完成初始化,线程二后续执行就会出现问题
  5. 线程一此时苏醒,完成后面的instence对象初始化的动做,并返回实例

然而在jdk1.5之后,这种状况有了解决方法,缘由在于jdk1.5开始针对volatile进行了加强,volatile变量开始能够屏蔽指令重排,也就是说

当咱们将instence引用进行volatile进行修饰的话instence = new DoubleCheck();这句话中的指令将不会被指令重排序,Double check也就再也不只是想一想了。附上完整代码:

3、静态内部类

静态内部类的优势是:外部类加载时并不会当即加载内部类,内部类不被加载就不去初始化实例,所以实现了懒加载。当StaticSingle第一次被加载时,并不须要去加载内部类Holder,只有当getInstance()方法第一次被调用时,才会致使虚拟机加载Holer类菜会去初始化StaticSingle实例。这种方法不只能确保线程安全,也能保证单例的惟一性,同时也延迟了单例的实例化。

那么静态内部类是如何实现线程安全的呢?咱们须要了解下面一些只是

针对于类的初始化,JVM虚拟机严格规定了有且仅有5种状况必须对类进行“初始化“:

  1. 遇到new、getstatic、setstatic或者invikestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,若是类没进行初始化,须要先调用其初始化方法进行初始化。
  3. 当初始化一个类时,若是其父类还未进行初始化,会先触发其父类的初始化。
  4. 当虚拟机启动时,用户须要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
  5. 当使用JDK 1.7等动态语言支持时,若是一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且这个方法句柄所对应的类没有进行过初始化,则须要先触发其初始化。

这5种状况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此以外的全部引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。

咱们再回头看下getInstance()方法,调用的是Holer.INSTANCE,取的是Holer里的INSTANCE对象,跟上面那个DCL方法不一样的是,getInstance()方法并无屡次去new对象,故无论多少个线程去调用getInstance()方法,取的都是同一个INSTANCE对象,而不用去从新建立。当getInstance()方法被调用时,Holer才在StaticSingle的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被建立,而后再被getInstance()方法返回出去,这点同饿汉模式。那么INSTANCE在建立过程当中又是如何保证线程安全的呢?在《深刻理解JAVA虚拟机》中,有这么一句话:

虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,若是多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其余线程都须要阻塞等待,直到活动线程执行()方法完毕。若是在一个类的()方法中有耗时很长的操做,就可能形成多个进程阻塞(须要注意的是,其余线程虽然会被阻塞,但若是执行()方法后,其余线程唤醒以后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞每每是很隐蔽的。

故而,能够看出INSTANCE在建立过程当中是线程安全的,因此说静态内部类形式的单例可保证线程安全,也能保证单例的惟一性,同时也延迟了单例的实例化。

那么,是否是能够说静态内部类单例就是最完美的单例模式了呢?其实否则,静态内部类也有着一个致命的缺点,就是传参的问题,因为是静态内部类的形式去建立单例的,故外部没法传递参数进去,例如Context这种参数,因此,咱们建立单例时,能够在静态内部类与DCL模式里本身斟酌。

4、枚举单例

从上述3种单例模式的写法中,彷佛也解决了效率或者懒加载以及线程安全的问题,可是它们都有两个共同的缺点:

  • 序列化可能会破坏单例模式,比较每次反序列化一个序列化的对象实例时都会建立一个新的实例,解决方案以下:

  • 使用反射强行调用私有构造器,解决方式能够修改构造器,让它在建立第二个实例的时候抛异常,解决方案以下:

如上所述,问题确实也获得了解决,但问题是咱们为此付出了很多努力,即添加了很多代码,还应该注意到若是单例类维持了其余对象的状态时还须要使他们成为transient的对象,这种就更复杂了,那有没有更简单更高效的呢?固然是有的,那就是枚举单例了,先来看看如何实现:

代码至关简洁,咱们也能够像常规类同样编写enum类,为其添加变量和方法,访问方式也更简单,使用EnumSingle.INSTANCE进行访问,这样也就避免调用getInstance方法,更重要的是使用枚举单例的写法,咱们彻底不用考虑序列化和反射的问题。枚举序列化是由jvm保证的,每个枚举类型和定义的枚举变量在JVM中都是惟一的。

在枚举类型的序列化和反序列化上,Java作了特殊的规定:在序列化时Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是经过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不容许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的惟一性,这里咱们不妨再次看看Enum类的valueOf方法:

public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                              String name) {
      T result = enumType.enumConstantDirectory().get(name);
      if (result != null)
          return result;
      if (name == null)
          throw new NullPointerException("Name is null");
      throw new IllegalArgumentException(
          "No enum constant " + enumType.getCanonicalName() + "." + name);
  }

实际上经过调用enumType(Class对象的引用)的enumConstantDirectory方法获取到的是一个Map集合,在该集合中存放了以枚举name为key和以枚举实例变量为value的Key&Value数据,所以经过name的值就能够获取到枚举实例,看看enumConstantDirectory方法源码:

Map<String, T> enumConstantDirectory() {
        if (enumConstantDirectory == null) {
            //getEnumConstantsShared最终经过反射调用枚举类的values方法
            T[] universe = getEnumConstantsShared();
            if (universe == null)
                throw new IllegalArgumentException(
                    getName() + " is not an enum type");
            Map<String, T> m = new HashMap<>(2 * universe.length);
            //map存放了当前enum类的全部枚举实例变量,以name为key值
            for (T constant : universe)
                m.put(((Enum<?>)constant).name(), constant);
            enumConstantDirectory = m;
        }
        return enumConstantDirectory;
    }
    private volatile transient Map<String, T> enumConstantDirectory = null;

到这里咱们也就能够看出枚举序列化确实不会从新建立新实例,jvm保证了每一个枚举实例变量的惟一性。再来看看反射到底能不能建立枚举,下面试图经过反射获取构造器并建立枚举

public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
   //获取枚举类的构造函数(前面的源码已分析过)
   Constructor<EnumSingle> constructor=EnumSingle.class.getDeclaredConstructor(String.class,int.class);
   constructor.setAccessible(true);
   //建立枚举
   EnumSingle singleton=constructor.newInstance("otherInstance",9);
  }

执行报错

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at zejian.SingletonEnum.main(SingletonEnum.java:38)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

显然告诉咱们不能使用反射建立枚举类,这是为何呢?不妨看看newInstance方法源码:

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        //这里判断Modifier.ENUM是否是枚举修饰符,若是是就抛异常
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

源码很了然,确实没法使用反射建立枚举实例,也就是说明了建立枚举实例只有编译器可以作到而已。显然枚举单例模式确实是很不错的选择,所以咱们推荐使用它。可是这总不是万能的,对于android平台这个可能未必是最好的选择,在android开发中,内存优化是个大块头,而使用枚举时占用的内存经常是静态变量的两倍还多,所以android官方在内存优化方面给出的建议是尽可能避免在android中使用enum。可是无论如何,关于单例,咱们老是应该记住:线程安全,延迟加载,序列化与反序列化安全,反射安全是很重重要的。

至此,单例模式的介绍完毕,不足之处你们补充指点。

参考:

https://blog.csdn.net/chenchaofuck1/article/details/51702129

https://blog.csdn.net/mnb65482/article/details/80458571

https://blog.csdn.net/javazejian/article/details/71333103#%E6%9E%9A%E4%B8%BE%E4%B8%8E%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F

《深刻理解Java虚拟机 JVM高级特性与最佳实践》

相关文章
相关标签/搜索