大家好,欢迎来到IT知识分享网。
单例模式(Singleton Pattern)是一种最简单又最常见的创建型设计模式,用来确保一个类只有一个实例。
大家可能会说,这么简单的一个入门级别的设计模式,有谁不会呢?再说了现在大家都是面向Springboot编程,直接用 #Spring 的依赖注入容器管理对象的生命周期,谁还自己写单例代码呢?
还请您别急!单例虽然简单,但是读完本文,你一定可以发现一些以前没留意到的知识点!
而且,虽然Spring用起来很方便,作为 #Java程序员,我们还是应该多了解一点基本功,提升自己的知识储备,您说对吧?
我们先总结一下单例在软件开发中主要的使用场景:
- 全局配置管理:共享全局的配置,确保配置的一致性。
- 日志管理:提供统一的日志接口、格式、输出、线程安全
- 资源管理器:比如管理连接池、线程池等有限的系统资源,避免频繁创建、销毁带来的性能消耗
- 硬件设备管理:硬件设备实例的管理,用来避免硬件资源访问的冲突。
当然以上这些只是一些常见的使用场景,在实际的开发工作中,面对不同的用例,总是会有一些独特的单例适应场景。
下面我们来看看单例是如何实现的
为了确保单例的唯一性,下面三个要点是单例的基础:
- 私有的构造函数:通过 private 的构造函数,禁止外部通过 new 创建对象。
- 静态成员变量:用来保存这个类的唯一实例。
- 静态的方法:提供一个公共的静态方法,用来获取唯一实例。
在满足上面三个基本要求后,就到了具体的实现方式上了
- 最简单的是懒汉式(Lazy Initialization)
为了节省内存占用,我们可能会在第一次使用到这个类的时候,才会创建实例对象,也就是延迟加载。

但是,这种方法的弊端很明显,大家都知道它的单例创建过程是线程不安全!
- 于是我们可以用饥饿加载模式-饿汉式(Eager Initialization)
在类加载的时候就马上创建实例,基于JVM的加载模式,这种实现模式下的单例创建过程肯定是线程安全的。

当然这种方法的弊端也很明显,就是提前加载实例,可能造成资源浪费,也可能增加更多的程序启动时间。
- 就有了最出名的双重检查锁定(Double-Checked Locking)
这个实现方式基本上是面试必问题了……

双重检查锁定基于延迟加载,基于synchronized实现了线程安全,用 volatile可以保证实例在不同线程中的可见性。但是对这种依赖,可能会在不同JVM实现中有些许的性能影响。可以点开《一文了解 Java并发编程:从 volatile 关键字到 Java 中的锁》了解更多。
那么有没有不加锁的延迟加载呢?有的!
- 用内部静态类实现单例

这种实现方式利用到了JVM的类加载机制:内部类只有在第一次使用到的时候才会进行加载。而在内部类加载的时候,就会初始化单例类的实例了。这种方式既确保了延迟加载,又确保了线程安全。
那么这种单例模式的实现方式是不是就是最完美的了呢?
不是!这种方式存在反射和反序列化破坏单例的漏洞:通过反射、或者反序列化,可以实现调用私有构造函数,从而破坏单例模式。
- 于是就有了通过枚举实现的单例模式

枚举模式的单例代码是最简洁的了,它不仅能避免多线程同步问题,还能防止反序列化和反射攻击。但是,这种实现方式又变成了饥饿加载的模式。而且这种方法还局限了功能扩展,因为枚举不能继承类。所以它比较适用于需要代码简洁、又要确保安全的情况下。
那么有没有无安全漏洞,又是延迟加载模式的单例实现呢?答案是:有的!
- 我们可以给单例的实现叠加以下两个额外的安全检查
- 如果单例类要实现 Serializable接口,我们可以实现resolve 方法来避免单例被破坏。
- 如果要防止反射攻击,我们可以在构造函数里面添加校验。

那么到这里,我们就已经完全了解单例模式了吗?不是的!
- 还有下面一些特殊的场景我们可能会需要用到特殊的单例实现!
- 如果我们的程序(比如 Android 开发)支持模块热加载、卸载,那么在模块卸载或禁用的时候,单例模式可能会导致类的回收出现问题。
- 对于内存敏感的系统,静态变量的单例模式会影响到内存的占用。
这个时候我们可能需要考虑使用弱引用的方式实现单例。

上面的代码是基于双重检查锁定的方法叠加弱用实现的单例。那么为什么不用性能更好的静态内部类(不用加锁)的实现呢?
主要是因为静态内部类的实现方式中有两个类,内部类中的实例是final的,一旦被垃圾回收,想要重建就会很麻烦,需要实现额外的处理逻辑。如果您有兴趣的话,可以尝试自己实现一下哦。
到这里我已经讲了 6 种单例实现方法加 1 种单例安全防护,有没有一些是您之前没有接触到呢?或者您还有其他典型的实现方法呢?
下面是各种实现方法的核心维度做个简单的对比表格:

那么说了这么多,单例有什么比较典型的缺点呢?
1. 违法了面向对象编程的原则
单例这个设计模式其实有点违反了面向对象编程中的单一职责的原则:类的创建和管理、业务逻辑都放在了一起。
所有很多时候,会和工厂模式一起使用,Spring的依赖注入容器就是一个典型的工厂模式。
2. 函数式编程中的冲突
函数式编程提出数据不可变,避免数据操作的共享。而单例强调的是全局共享,与函数式编程理念冲突。
所以在函数式编程中我们要尽量避免使用单例,减少共享状态。
设计模式的核心在于解决问题,没有完美的设计模式,只有适合我们业务场景的解决方案。单例模式作为设计模式中的看门员,过去、现在和未来,都会有它适应的场景,所以了解多一点,才能在选择是否使用的时候更有把握一点。
感谢您的耐心阅读,如果您有更多的心得,欢迎分享,共同进步:)
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/179448.html