王争《设计模式之美》学习笔记
为什么要使用单例?
- 单例设计模式(Singleton Design Pattern):一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
实战案例一:处理资源访问冲突
文中样例
- 一个往文件中打印日志的 Logger 类。
- 构造方法中,打开日志文件 log.txt,将写入文件句柄赋值给属性 writer。
- log() 方法中调用 writer.write() 写入日志内容。
- 使用此 Logger 类时,首先实例化 new Logger(),然后调用log() 方法写入日志。
样例中问题及解决方案
- Logger 类中的日志都写到同一个文件 log.txt 中,在多线程环境下,当 Logger 类被多处调用者实例化后,大家同时写 log.txt,可能会存在互相覆盖。
- 首先我们想到的是加锁方案,在 log() 方法中调用 writer.write() 前加入一个对象锁。但是在不同线程下,不同对象调用并不会共享一把锁,问题依然存在。
进一步解决方案
- 换成类级别锁,让所有对象都共享一把锁。 具体方案,在 log() 方法中调用 writer.write() 前加入一个类锁。
- 分布式锁也是一种解决方案,不过,实现一个安全可靠、无bug、高性能的分布式锁,并不是件容易的事情。
- 并发队列解决方案,多个线程同时往并发队列写日志,一个单独线程将并发队列中数据写入日志文件,也是一个稍复杂的方案。
单例模式解决方案
- 增加 getInstance() 方法,此方法中返回属性 instance。
- 而 private static final Logger instance = new Logger()。
- 调用的时候直接 Logger.getInstance().log(),这样 Logger 类就只会被实例化一次。
- 好处是可以不用创建那么多 Logger 对象,一方面节省内存空间,另一方面节省系统文件句柄。
实战案例二:表示全局唯一类
- 业务场景:
- 有些数据在系统中只应保存一份,比如配置信息类。
- 唯一递增ID号码生成器,不能生成重复的ID。
- 文中样例:
- IdGenerator 类中 getInstance() 方法返回 instance。
- 而 private static final IdGenerator instance = new IdGenerator()。
- 获取 id 方法 getId() 中,返回 id.incrementAndGet()。
- 使用的时候,直接 IdGenerator.getInstance().getId()。
如何实现一个单例?
实现一个单例关注点:
- 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例。
- 考虑对象创建时的线程安全问题.
- 考虑是否支持延迟加载.
- 考虑 getInstance() 性能是否高(是否加锁)。
饿汉式
- 加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。这样的实现方式不支持延迟加载。
- 有的人认为,提前初始化实例是一种浪费资源的行为,最好的方法应该在用到的时候再去初始化。理由是实例占用资源多或者初始化耗时长:但是如果初始化耗时长,那我们更不能等到要用它的时候再初始化,会影响到系统的性能。如果实例占用资源多,那我们希望在程序启动的时候就能触发报错。
- 我们前文的例子都是饿汉式。
懒汉式
- 懒汉式相对于饿汉式的优势是支持延迟加载。
- 以前文中的 IdGenerator 类举例,在 getInstance() 方法中才会 new IdGenerator() 赋值给属性 instance。
- 不过这样的缺点显而易见,我们给 getInstance() 方法加了锁,导致并发度很低,相当于串行。如果这个单例类会被频繁用到,那么频繁加锁、释放锁会降低并发,导致性能瓶颈。
双重检测
- 在这种实现方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中。即支持延迟加载,又解决并发度低的问题。
- 我们在 getInstance() 方法中,new IdGenerator() 前加入类级别锁。
静态内部类
- 一种比双重检测更加简单的实现方法,那就是利用 Java 的静态内部类。
- 在 IdGenerator 类内部创建一个静态内部类 SingletonHold,在此静态内部类里面添加属性 private static final IdGenerator instance = new IdGenerator()。
- 当类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。
枚举
- 一种最简单的实现方式,这种实现方式通过 Java 枚举类型本身的特性,基于枚举类型的单例实现。
- 在 IdGenerator 类中定义枚举类型 INSTANCE。