设计模式–单例模式

要搞清楚以下几个问题?

  • 为什么要使用单例
  • 单例存在哪些问题
  • 单例与静态类的区别
  • 有何替代的解决方案

什么是单例模式

单例设计模式:一个类只允许创建一个对象(实例),那这个类就是一个单例类,这种设计模式就是单例设计模式。

。。。。

实例1:避免i资源访问冲突

将对象级别的锁换成类级别的锁就行,让所有的对象都共享一把锁。这样就避免了不同对象之间同时调用log()函数,而导致的日志覆盖问题

单例模式的解决思路就简单一些了。单例模式相对于之前类级别锁的好处是,不用创建那么多Logger对象,一方面节省内存空间,另一方面节省系统文件句柄(对于OS来说,文件句柄也是一种资源,不能随便浪费)

我们将Logger设计成一个单例类,程序中只允许创建一个Logger对象,所有的线程共享使用这一个Logger对象,共享一个FileWriter对象,而FileWriter本身是对象级别线程安全的,也就避免了多线程情况下写日志会相互覆盖的问题。

1
2
3
4
5
6
7
8
9
10
11
public class Logger{
private FileWriter writer;
private static final Logger instance = new Logger();

private Logger(){
File file = new File("xxx/log.txt");
writer = new FileWriter(file,true);//true表示追加写入
}

//未写完
}

实例2:表示全局唯一类

从业务概念上,如果有些数据在系统中只应保存一份,那么就比较适合设计为单例类

如:

  • 配置信息类,
  • 唯一递增ID号码生成器

如何实现一个单例

概括起来,要实现一个单例,我们需要关注的点无外乎下面几个

  • 构造函数需要是private访问权限的,这样才能避免外部通过new创建实例
  • 考虑对象创建时的线程安全问题
  • 考虑是否支持延迟加载( 延迟加载的意思是说,在关联查询时,利用延迟加载,先加载主信息。使用关联信息时再去加载关联信息 )
  • 考虑getInstance()性能是否高(是否加锁)

五种单例实现方法

饿汉式

饿汉式的实现方式比较简单,在类加载的时候,instance静态实例就已经创建并初始化好了,所以,instance实例的创建过程是线程安全的。不过这样的实现方式是不支持延迟加载的。

有人认为如果实例占用资源多(如内存)或者初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该是在用到的时候再去初始化

作者则认为:如果初始化耗时长,那最好不要等到真正要用到它的时候,采取执行这个耗时长的初始化过程,这会影响到系统的性能(如:在相应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时);采用饿汉式实现方式,将耗时的初始化操作提前到程序启动的时候完成,这样就能避免程序运行的时候,再去初始化导致的性能问题

如果实例占用资源多,按照fail-fast的设计原则(有问题及早暴露),我们希望在程序启动时就将这个实例初始化好,如果资源不够,也会正在程序启动的时候触发报错,我们可以立即取修复

懒汉式

懒汉式对比饿汉式的优势是支持延迟加载

不过这种方式的并发度很低,类似串行

如果这个单例类是偶尔会被使用,这种设计是可以被接受的,但是如果被频繁使用,那么频繁加锁,释放锁及并发度低等操作,会导致性能瓶颈,这种实现方式不可取

双重检测

既支持延迟加载,又支持高并发的单例实现方法

主要instance被创建之后,再调用getInstance()函数都不会进入到加锁逻辑中,所以这种实现方式解决了懒汉式并发度低的问题

静态内部类
枚举

通过java枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性

单例存在哪些问题

  1. 单例对OOP特性支持不友好