单例模式是面试中问的最多的设计模式之一,同时它也是最基础的设计模式。说它基础,并不意味着简单,涉及到的延伸知识点非常多。如果要深究下去,可以把Java的基础知识问个遍。本文不作特别细致的讲解,抛砖引玉提几点比较重要的地方,以记录第一次在工作中的使用。
1、什么是单例模式?
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
2、为什么要用单例模式?
对啊,为什么要使用?其实我刚开始的时候也不知道。后来具体工作研究中就会慢慢发现一点。首先思考这样一个问题,为什么不用静态方法?这样就彻底杜绝了单例模式的使用,直接通过类调用静态方法即可。因为静态方法贮存在内存中,不会被GC,生命周期跟随它的类。如果创建大量的静态方法,会占用大量的资源,导致内存溢出。有人可能会问,某些项目中充斥着大量的静态方法,为什么也用的顺畅自如?这个,这个我也不知道,我个人认为可能是项目的并发量并没有那么大,使得静态方法的调用方法没那么大。当然,这个也是可以测试的。不断的for循环呗,观察内存的使用率情况。(推荐试试)综上,还是尽量少用静态方法。那么引申出来的第二个问题:既然用实例化方法,那就让它创建实例啊,为什么还用单例模式?标准答案如下:
- 由于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级的对象而言,是很重要的;
- .因为不需要频繁创建对象,我们的GC压力也减轻了,而在GC中会有STW(stop the world),从这一方面也节约了GC的时间 。
换言之,我们有时候并不需要多个实例,一个即可。比如数据库连接池,整个系统项目中,只要一个就够了。多个的话创建关闭都会花费时间,浪费资源。于是什么时候使用单例模式的问题也就出来了。在你觉得只需要一个实例即可的时候,就大胆地去用。
3、单例模式的实现
翻阅了网上所有的博客,都大同小异,什么饿汉,懒汉,doublecheck等等。其实我个人认为这个要分为区分的维度,按照什么维度分,就会有什么答案。中国人的思维仿佛永远是这样,非要分个一二三点出来。。。
(1)按照加载类的角度:饿汉,懒汉。
有些需要在类加载的时候就能立刻进行实例化,借此保证线程安全。因为类加载过程JVM会自动加锁,因此保证了单例特性。
有些则不需要,因为有些类的实例化耗费资源,浪费时间,实际上可能还并不会用到,因为会推迟到使用的时候才进行实例化。
饿汉代码:
static字段会在类加载的时候new出来。
懒汉代码:
见下面第二个维度的各种实现。
(2)实现的方式(懒汉)
线程不安全:
线程安全:
Double check::
volatile关键字保证了指令的顺序,不会发生指令重排的情况。分配空间-》初始化-》指针指向。否则后面两项会有重排的风险。
静态内部类:(推荐)
这种方式也是根据类加载时候的初始化锁保证线程安全的,请参考《深入理解JVM虚拟机》一书。
枚举类:(推荐)
枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。那么问题来了,单例模式怎么会被破坏呢?反射!
4、破坏单例模式
实际上使用反射去创造对象就会破坏单例模式,代码如下:
除了枚举以外,其它都会别破坏,如果枚举类利用反射则会报错,如下:
那么如何阻止反射呢?
静态内部类可以在私有构造器加代码如下:
其它几种方法因为反射仍然会修改内部字段,所以没想到好的方法,有想法欢迎评论。
还一种序列化破坏单例模式的例子:
主要原因是readObject方法中存在这样的调用链:
readObject->readObject0->readOrdinaryObject
readOrdinaryObject方法会利用反射的方式创造无参对象,但是接下来会有一个判断,要实现的单例类是否会存在readResolve这一一个方法,如果实现则会调用。因此防止序列化破坏单例模式的解决方法是加上这一一个方法。
单例模式虽然基础,但是并不简单,涉及到的知识点非常多。比如初始化锁的实现、枚举类是如何得到唯一实例的等等本文并没有详细解释,希望读者朋友有兴趣深入理解并搞懂搞透。
注:本文参考了部分博客的叙述,如有不妥之处,请作者告之,会及时作出处理。谢谢。