一个单例还能写出花来吗?(一)
单例可以说是最简单的一个设计模式了,单例模式要求只能创建一个对象实例。通常的写法是声明私有的构造函数,提供静态方法获取单例的对象实例。
常见的单例写法就是饿汉式、懒汉式、双重加锁验证、静态内部类和枚举的方式,写法可能大家都知道,不过针对不同的写法还是有可以继续深挖一下的地方,让我们从最简单的几种写法开始回顾单例,不想看前面的话直接往后翻好了。
回顾几种实现方式
饿汉式
饿汉式的写法通常静态成员变量已经是初始化好的,优点是可以不加锁就获取到对象实例,线程安全,主要的缺点在于不是延加载,稍微存在内存的浪费,因为如果初始化的逻辑较为复杂,比如存在网络请求或者一些复杂的逻辑在内,就会产生内存的浪费。
懒汉式
懒汉式的写法解决了饿汉式浪费内存的问题,在真正需要获取实例对象的才去执行初始化。
通常一般来说可能会有两种方式,第一种就是不加锁的写法,很显然这样是肯定不行的,正常的方式一般都是通过同步锁的方式加锁获取实例对象。
但是这种实现方式在之前的JDK版本synchronized没有锁优化的情况每次获取单例对象性能存在很大的问题,于是乎有了DCL的写法。
双重加锁验证DCL
于是为了解决懒汉式性能的问题,双重加锁验证的写法诞生了,先判断一次空,真的为空再执行加锁,然后再判断一次。
这样的话,只有在实例对象是空的情况才会去加锁创建对象,性能问题得到了一定程度上的解决,也不会和饿汉一样有内存浪费的问题。
但是,这个写法也存在问题,就是会拿到未初始化完全的对象,我之前的一篇文章中也提到这个方式的问题,具体请看一次群聊引发的血案。
让我这里复用一下我写过的东西。
从CPU的角度来看,instance = new Instance()可以分为分为几个步骤:
- 分配对象内存空间
- 执行构造方法,对象初始化
- 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,得到如下内容:
文章转自公众号:艾小仙