乐趣区

关于java:一个单例还能写出花来吗

单例能够说是最简略的一个设计模式了,单例模式要求只能创立一个对象实例。通常的写法是申明公有的构造函数,提供静态方法获取单例的对象实例。

常见的单例写法就是饿汉式、懒汉式、双重加锁验证、动态外部类和枚举的形式,写法可能大家都晓得,不过针对不同的写法还是有能够持续深挖一下的中央,让咱们从最简略的几种写法开始回顾单例,不想看后面的话间接往后翻好了。

回顾几种实现形式

饿汉式

饿汉式的写法通常动态成员变量曾经是初始化好的,长处是能够不加锁就获取到对象实例,线程平安,次要的毛病在于不是延加载,略微存在内存的节约,因为如果初始化的逻辑较为简单,比方存在网络申请或者一些简单的逻辑在内,就会产生内存的节约。

懒汉式

懒汉式的写法解决了饿汉式节约内存的问题,在真正须要获取实例对象的才去执行初始化。

通常一般来说可能会有两种形式,第一种就是不加锁的写法,很显然这样是必定不行的,失常的形式个别都是通过同步锁的形式加锁获取实例对象。

然而这种实现形式在之前的 JDK 版本 synchronized 没有锁优化的状况每次获取单例对象性能存在很大的问题,于是乎有了 DCL 的写法。

双重加锁验证 DCL

于是为了解决懒汉式性能的问题,双重加锁验证的写法诞生了,先判断一次空,真的为空再执行加锁,而后再判断一次。

这样的话,只有在实例对象是空的状况才会去加锁创建对象,性能问题失去了肯定水平上的解决,也不会和饿汉一样有内存节约的问题。

然而,这个写法也存在问题,就是会拿到未初始化齐全的对象,我之前的一篇文章中也提到这个形式的问题,具体请看一次群聊引发的血案。

让我这里复用一下我写过的货色。

从 CPU 的角度来看,instance = new Instance()能够分为分为几个步骤:

  1. 调配对象内存空间
  2. 执行构造方法,对象初始化
  3. instance 指向调配的内存地址

实际上,因为指令重排的问题,2、3 的步骤可能会产生重排序,那么问题就产生了。

instance 先被指向内存地址,而后再执行初始化,如果此时另外一个线程来拜访 getInstance 办法,就会拿到 instance 不是 null,最初拿到的将是一个没有被齐全初始化的对象!

当初也有很多人说这个问题在高版本的 JDK 中曾经解决了,然而我是没发现有什么间接证据,如果你晓得,请你通知我。

动态外部类

这个通过 JVM 来保障创立单例对象的线程平安和唯一性,是比拟好的方法。

Singleton类加载的时候,SingletonHolder不会加载,只有在调用 getInstance 办法的时候才会执行初始化,这样既起到了懒加载的作用,同时又应用到了 JVM 类加载机制,保障了单例对象初始化的线程平安。

这种形式也是目前比拟举荐的一种形式。

枚举

通过枚举来实现单例是 Effective Java 作者 Josh Bloch 提倡的形式,也是单例模式的最佳实现形式。

为了看清楚枚举怎么实现单例模式的,咱们来编译一下枚举生成的最终字节码。

执行 javac Singleton.java 生成 class 文件,接着执行 javap -p Singleton.class,失去如下内容:

为了看到更具体的内容,咱们执行 javap -c Singleton

通过最终生成的字节码,咱们其实发现实质上枚举的初始化通过 static 代码块来进行初始化。

思考下类加载的几个步骤,加载 -> 验证 -> 筹备 -> 解析 -> 初始化 ,最终初始化就是执行static 代码块,而 static 代码块是相对线程平安的,只能由 JVM 来调度,这样保障了线程平安。

枚举的实现形式益处还不止于此,除了高深莫测的实现简略之外,还能避免其余几种实现形式防止不了的几个问题。

再说几种形式的问题

反射毁坏单例

除了枚举之外,其余的几种形式都能够通过反射的形式达到毁坏单例的目标,就轻易以一个实现形式来举例,这里最终的输入后果是false

如果拿去尝试反射创立枚举对象的话,则是会报错,能够本人入手尝试一下。

为什么会报错,能够间接看一下 newInstance 的源码,有一段非凡的对于枚举类型的判断,下图中我红色标记的局部。

序列化

除了家喻户晓的应用反射来毁坏单例之外,还有另外一种能毁坏单例的形式就是序列化。

对下面的饿汉办法实现序列化,而后失去的后果是false,序列化前后对象产生了扭转。

其实要害的局部在于 ois.readObject 办法,一路跟踪最初找到一段代码如下:

所以很显著咱们发现了最终实际上这里通过反射创立了一个新的对象,isInstantiable理论代表的应该是类或者属性是序列化的,那么久就返回 true,咱们这里必定是 true,所以最终产生了一个新的对象。

枚举为啥能够避免这个问题?枚举的实现形式不太一样而已,同样跟踪到枚举局部的实现逻辑。

下图中红框标注的局部就是枚举类型去实现反序列化的逻辑,最终只是通过 valueOf 办法查找枚举,不存在新建一个对象的逻辑。

那么,怎么避免其余形式序列化对单例的毁坏?再往下看看源码,红框标注的意思只有有 readResolve 办法就能够解决问题了。

实际上,最终解决方案也很简略,单例类加上办法即可。

好了,打完出工。当初是北京工夫 4 月 15 日凌晨 1 点整,困了,睡觉。

退出移动版