Singleton可以说是最简单的设计模式了。单例模式只需要创建一个对象实例。通常的写法是声明一个私有构造函数,提供一个静态方法来获取单例对象实例。常见的单例写法有饿汉式、懒汉式、双锁验证、静态内部类和枚举。写法大家可能都知道,但是对于不同的写法,还是有可以继续深挖的地方。让我们从最简单的写法开始复习单例。如果你不想看前面的话,就把它们翻回去。回顾几种实现方式饿了么中式饿了么中式的写法通常是静态成员变量都已经初始化了,优点是可以不加锁的获取对象实例,线程安全,主要缺点是不是懒加载,有是有点内存浪费,因为如果初始化逻辑比较复杂,比如网络请求或者一些复杂的逻辑,就会产生内存浪费。Lazy-style懒惰式的写法解决了饥饿式浪费内存的问题,只有在真正需要获取实例对象时才进行初始化。一般来说,可能有两种方式。第一种是无锁写。显然,这绝对是不可能的。正常的方式是通过同步锁来锁定和获取实例对象。但是在之前的JDK版本synchronized没有锁优化的情况下,每次获取单例对象在性能上有很大的问题,于是就有了写DCL的方式。双锁验证DCL于是为了解决懒惰的性能问题,诞生了双锁验证的写法,先判断为空,如果真的为空则执行加锁,再判断。这样,只有当实例对象为空时,才会锁定并创建对象。性能问题得到了一定程度的解决,内存浪费的问题不会像饿了么一样。不过这种写法也有一个问题,就是会得到未初始化的对象。我在之前的文章中也提到了这种方式的问题。具体可以参考一个群聊引发的血案。让我重用我在这里写的东西。从CPU的角度看,instance=newInstance()可以分为几个步骤:分配对象内存空间并执行构造方法,对象初始化实例指向分配的内存地址实际上,由于问题instructionrearrangement,2.3的步骤可能会重新排序,然后问题就出现了。该实例首先指向内存地址,然后进行初始化。如果此时有另一个线程访问getInstance方法,则实例不为null,最后一个对象就是一个没有完全初始化的对象!现在很多人说这个问题在高版本的JDK中已经解决了,但是我没有找到直接的证据,有知道的请告诉我。静态内部类是通过JVM创建单例对象保证线程安全性和唯一性的更好方式。加载Singleton类时,不会加载SingletonHolder,只会在调用getInstance方法时初始化,既起到了懒加载的作用,又利用JVM类加载机制保证了线程安全单例对象初始化。该方法是目前推荐的方法。枚举通过枚举实现单例是《EffectiveJava》的作者JoshBloch提倡的方式,也是实现单例模式的最佳方式。为了看看枚举是如何实现单例模式的,我们来编译枚举最终生成的字节码。执行javacSingleton.java生成一个class文件,然后执行javap-pSingleton.class得到如下内容:为了看到更详细的内容,我们执行javap-cSingleton。通过最终生成的字节码,我们其实发现枚举的初始化本质上是通过静态代码块来初始化的。考虑下面的类加载步骤,loading->verification->preparation->analysis->initialization,最后的初始化是执行静态代码块,静态代码块是绝对线程安全的,只能被调度JVM,从而保证线程安全。实施枚举的好处不仅限于此。除了一目了然的简单实现之外,它还可以防止其他实现无法避免的几个问题。下面说说几种销毁单例的方法的问题。除了枚举之外,还有其他几种方式可以通过反射达到销毁单例的目的。我们仅以一个实现方法为例。这里的最终输出是错误的。.如果尝试通过反射创建枚举对象,会报错,大家可以自己试试。为什么报错,可以直接看newInstance的源码,里面对枚举类型有专门的判断,下图中我标红的部分。除了众所周知的使用反射来销毁单例之外,还有一种方法是序列化来销毁单例。序列化上面的hungry方法,结果为false,序列化前后对象发生了变化。其实最关键的部分就在ois.readObject这个方法上。一路追踪,终于找到了一段代码如下:所以很明显我们发现其实是通过反射创建了一个新的对象。isInstantiable其实代表类或者属性被序列化了,然后长时间返回true,我们这里一定是true,所以最终生成了一个新的对象。为什么枚举可以避免这个问题呢?枚举的实现不同,同样跟踪枚举部分的实现逻辑。下图中红框标注的部分就是枚举类型反序列化的逻辑。枚举最终只是通过valueOf方法进行查找,并没有创建新对象的逻辑。那么,如何防止其他序列化方法破坏单例呢?往下看源码,红框的意思是只要有readResolve方法就可以解决问题。其实最终的解决方案也很简单,只需要在单例类中添加一个方法即可。好吧,收工吧。现在是北京时间4月15日凌晨1点。我困了,要睡觉了。
