相信很多同学在学习设计模式的过程中,最先了解的就是单例模式,至少我是这样。单例模式看似简单容易理解,实际上却有很多坑,正因为这样也成为了很多公司面试必考的面试题。实际使用单例模式时,难免会遇到滥用的情况,理解单例更要学会何时拒绝单例。
0x01单例模式的基础知识
1. 单例的几种写法?
- 懒汉式: 懒加载模式,需要的时候才创建实例。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。需要考虑线程安全问题。
- 饿汉式:单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
- 静态内部类:JVM本身机制保证了线程安全问题,由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
2. 为什么要双重检验锁?
1 | public static Singleton getSingleton() { |
- 第一个条件是说,如果实例创建了,那就不需要同步了,直接返回就好了。
- 不然,我们就开始同步线程。
- 第二个条件是说,如果被同步的线程中,有一个线程创建了对象,那么别的线程就不用再创建了。
3. 重排序问题
1 | instance = new Singleton() |
这并不是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
对此,我们只需要把singleton声明成 volatile 就可以了。
0x02 滥用单例带来哪些问题?
你的项目中可能会有一堆的Manager或者Controller,最简单获取方法是把他们设计成一个个单例,只需要通过一个getInstance的方法获取到他的唯一实例对象,在任何代码中的任何地方,甚至不需要上下文。是否思考过下面的这些问题?
1.你的单例会有应用的生命周期吗?
按照单例的定义,无法构建除该单例以外的实例,并且这个单例有一个静态引用,单例不会被虚拟机垃圾回收。单例对象一旦创建,对象的引用是保存在静态区,单例对象在堆上分配的内存空间只有在程序终止后才会释放,过多的单例必然增大内存的消耗,并且如果你的单例中的上下文引用了不当,可能会造成严重的内存泄露问题。单例的设计应该只用来保存全局的状态,并且不能和任何作用域绑定。如果这些状态的作用域比一个完整的应用程序的生命周期要短,那么这个状态就不应该用单例来管理。
2.如何尽可能的减少依赖【耦合】?
有依赖就有耦合,如果你的单例提供的是某一个特定功能的公共方法或状态。那调用方和被调用方就会因为单例的设计建立了强耦合关系。甚至更极端的情况中,包含了多个单例之间的相互依赖甚至循环依赖关系。那你永远不可能将各种角色模块化拆分出来。
3.如果你想针对你的单例做扩展或升级怎么办?
对单例的实现升级其实并不是单例模式要解决的问题,但是单例模式缺少抽象,在使用过程中必然会遇到这样的问题。在业务的发展过程中,一定会遇到需要对已有单例进行实现升级的情况,比如你需要对相同功能换一种方式实现?或者需要增加删除一个方法或接口?甚至可能变成一个完全不一样的功能。这时候的单例就会遇到一些问题,你不单单需要修改单例,你还需要修改依赖单例的上层业务代码。
0x03如何尽可能少的使用单例模式?
单例的好处是简单易用,单例模式在系统设置了全局的访问点,优化和共享资源的访问。但是滥用单例也带来的上述的三个方面的危害,无法统一管理对象的生命周期、增加耦合、针对实现编程而不是针对接口编程。如何享受单例模式带来的好处并且解决她带来的危害在于,如何通过一种解耦的方式全局获取一个单例对象,并且这个单例对象的生命周期是可管理的,并且这个单例对象是针对接口的抽象实现。
- 针对接口实现:单例不再提供getInstance方法,并且根据不同的抽象接口实现
- 提供全局的访问点: 调用方可以通过全局的SingleManager获取实现接口的具体实例
- 可管理的对象生命周期: 所有的单例的唯一对象都由SingleManager管理
- 低耦合的:为了避免循环引用问题,SingletonManager在运行时根据配置文件通过反射的方式实例化每个接口的具体实现类,调用方只依赖SingletonManager,并且通过SingletonManager获取接口的实现实例