简约不简单的单例模式

2个月前 ( 03-17 ) 280阅读 0评论

【CSDN 编者按】六种模式演绎单例模式,看完你还觉得它简单吗?

作者 | 刘钊,中国农业银行研发中心,高级工程师

责编 | 欧阳姝黎

出品 | CSDN(ID:CSDNnews)

看到标题大概率有同学不服,心里想单例模式不是最简单的设计模式吗?学习单例模式之前我是这么认为的,学习之后我发现我有些“轻敌”了。单例模式设计方法之多,涉及知识之深入,是之前不曾料到的。单例模式的“别有洞天”,称得上是简约不简单,低调奢华有内涵。

单例模式又名单件模式或单态模式。英文名称为:Singleten Pattern。单例模式用于创建在软件系统中独一无二的对象。单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在生活中,单例的场景有一个学校只有一名正校长,如果出现了多个正校长,那就乱了套了。在系统建设中,单例模式的应用就太广了,从网站计数器、打印机任务队列、数据库连接类等。单例模式除了可以消除混乱,还可以减少系统资源的消耗,是一种非常常用的设计模式。

在这里我介绍了6种单例模式实现方法,每种方法都有自己的特点,让我们一起领略一下他们的风采。

单例模式第一式:饿汉式

这是大家见得最多的,实现起来最简单的单例实现方式,被各种设计模式教材收录。饿汉式其实还有一个名字叫预加载技术。也就是说系统启动后,单例对象就已经创建完毕,无论是否以后会被使用。源码示例如下:

这里就涉及一个知识点,所有的静态成员在类加载完成之后都已经完成了初始化赋值的操作。也就是EagerSingleton类的私有构造函数会被调用,实例会被创建。

再提醒大家特别注意一个小细节,EagerSingleton的构造函数,是private修饰的。这样做的一个很重要的目的,就是防止使用new关键字来创建一个新的实例,造成“多例”并存的局面。几乎所有的单例模式,均是如此,后面就不再着重说明了。

别小看饿汉式单例,他的优点可不少,除了实现难度低,容易理解之外,其实他还有线程安全的特点,虽然启动时耗费了初始化的工作,但是在高并发场景下,为系统提供了一种非常可靠的方式。饿汉式单例不仅应用广泛,也是被推荐使用的方式之一。

单例模式第二式:懒汉式

懒汉式单例模式也是非常经典的一种单例模式。懒汉式单例在类加载时不进行实例化,而是使用者第一次调用getInstance方法时进行实例化。这种延迟加载技术可以提高系统启动速度,是系统开发中一种常见的资源利用效率提高方法。源码示例如下:

虽然懒汉模式在资源利用率上有一定优势,但是懒汉模式在高并发场景,也就是多线程场景下,遇到的问题也比较多,必须加以了解和重视。为了防止多个线程同时调用getInstance方法,需要在该方法前面增加关键字synchronized进行线程访问锁定。但是这样就引出了新的问题,在高并发场景下,每次调用都进行线程访问锁定判断,会对系统性能产生较大的负面影响。

也就是说,上面这种懒汉式单例实现方法,虽然是线程安全的,但是对于高并发场景并不友好,一般不建议使用。

单例模式第三式:双重检查锁定式

针对以上懒汉式存在的问题,有一个方法可以解决,那就是使用synchronized关键字,只锁定部分代码。如下所示:

问题看起来得以解决,但实际这个方法是有线程安全问题的,是不可以用于生产场景特别是高并发场景的。假设有两个线程A和B,同时通过了instance对象为null的判断,因为有synchronized加锁机制,所以线程A先进入了synchronized锁定的代码中,进行实例创建。线程B处于排队等待状态。线程A执行完毕后,线程B进入锁定代码,这时线程B并不知道实例已经被创建,会再次创建新的实例,发生了“多例”的情况。

所以在synchronized锁定代码中,需要再次进行是否为null检查。这种方法叫做双重检查锁定(Double-Check Locking)。完整代码如下:

单例模式第四式:静态内部类实现式

下面要介绍的这种实现方式,既没有饿汉式单例无论是否使用都要占用内存的问题,也不存在上述懒汉式单例性能问题和线程安全控制复杂的问题。可谓是Java语言下一种非常好的单例模式实现方法。各位同学不要激动,大家先看一下源码示例:

是不是脑瓜子嗡嗡的?反正笔者在第一次看得时候,是看不太懂的。

让我简单解释一下,首先大家看一下代码里面是有一个静态内部类的,而真正的实例变量,是定义在这个静态内部类中的。各位细心的同学可能有疑问了,实例变量是被static修饰的,那应该是被预加载才对,怎么这个实现方法说是懒加载呢?这里有个小知识点就是内部类前面加static关键字,表示的是类级内部类,类级内部类只有在使用时才会被加载。

那又有优秀的同学来问了,既然是懒加载了,那么就可能遇到高并发场景,如何保证线程安全呢?简单说就是静态变量的初始化是由JVM保证线程安全的,具体的细节就不展开讲了,有兴趣的同学可以自行深入了解一下。

总结一下,当getInstance方法第一次被调用的时候,它第一次读取InstanceHolder.instance,导致InstanceHolder类得到初始化。而InstanceHolder类在装载并被初始化的时候,会初始化它的静态域,从而创建HolderSingleton的实例。由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。这个模式的优势在于,getInstance方法并没有做线程同步控制,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

相对于双重检查锁定式,本方法实现单独更低,如果是需要延迟加载,更推荐使用此方法。硬要给这种实现方式找个缺点的话,那就是需要编程语言的支持。

单例模式第五式:枚举式

下面要介绍的这种方式,让人看了直喊666,陷入计算机的神奇世界久久不能自拔,直接上源码示例震撼一下:

没错,就是这么简约(但是不简单)。大体介绍一下原理,首先创建Enum时,编译器会自动为我们生成一个继承自java.lang.Enum的类,枚举成员声明中被static和final所修饰,根据在静态内部类式单例中讲过的,虚拟机会保证这个成员在多线程环境中被正确的加锁和同步,所以是线程安全的。类似于如下效果:

另外,Enum的构造方法本身就是private限制的,所以也防止了使用new关键字创建新实例。从Enum类的声明中我们也可以看出,Enum是提供了序列化的支持的,在某些需要序列化的场景下,提供了非常大的便利。另一个重要功能就是反序列化仍然可以保证对象在虚拟机范围内是单例的。

总之,借用Joshua Bloch在《Effective Java》中的一句话:单元素的枚举类型已经成为实现单例模式的最佳方法。

单例模式第六式:单例注册表式

提起单例注册表方式,大部分同学可能没有怎么听说过,可能觉得这是哪里来的“偏方”?但是如果我说这种方式是被大名鼎鼎的Spring框架使用,是不是你们就觉得有意思了。还是让我们来领略一下单例注册表的风采:

虽然大家接触这种模式比较少,代码看起来比其他方式要多一些,但是实现原理还是比较容易理解的。就是以一个HashMap来存储目前已生成的类的实例,如果可以根据类名找到对象,就返回这个对象,不再创建新对象。如果找不到,就利用反射机制创建一个,并加入到Map中。以上只是一个示意代码,作为Spring核心理念IoC的重要部分,单例注册表在Spring中的源码要复杂的多,也做了很多性能上的优化,有兴趣的同事可以去看一下Spring中AbstractBeanFactory类的源码。

不知道大家学完以上几种单例模式之后有什么感觉,这些不同的实现方式,如果要认真学习,彻底搞明白的话,涉及的知识点还是非常多的。有很多知识也不是我们平时做业务研发能接触到的。

怎么样,单例模式也是可以很炫酷的。

预约《大咖来了》直播,赢纪念版卫衣以及保温杯等礼品,在直播间精选留言提问题,若问题被采纳,将直接赠送马克杯!先到先得!



☞左手代码,右手带娃,还能发十几篇 paper,程序员女神是如何炼成的?
☞MIPS 已死,转身投靠 RISC-V!
☞没有特斯拉的 3·15 都曝了些什么?
☞如何以出售开源软件为生?
文章版权声明:除非注明,否则均为七默网原创文章,转载或复制请以超链接形式并注明出处。

发表评论

表情:
评论列表 (暂无评论,280人围观)

还没有评论,来说两句吧...

取消
支付宝二维码
支付宝二维码
微信二维码