单例模式定义
单例模式(Singleton pattern)限制了类的实例化,并且确保了该类有且仅有一个实例存在。单例类必须提供一个获取实例的全局入口点(global access point)。单例模式类图如下
使用场景
单例模式在应用程序中的使用还是很广的,比如应用的日志管理应用、数据库连接池、线程池等等,这些都应该只有一个实例去操作。
单例模式的实现由以下几个要点:
- 私有的构造器保证其他类不能对单例类进行实例化
- 私有的静态变量确保该类只有一个实例
- 返回该类实例的公有的静态方法,提供给其他类获取单例的接口
单例模式的实现(Implement)
下面介绍多种单例模式的实现
饿汉式单例(Eager Initialization)
这种单例模式实现是最简单的,在饿汉式单例中,单例类的实例在类加载的时候就已经实例化了,虽然这种方法是线程安全的,但是有一些场景饿汉式单例就无法使用了。比如我们的Singleton实例的创建是依赖参数或者配置文件,在getInstance()之前必须动态调用某个方法,这样饿汉式单例就无法使用了。
代码实现
|
|
总结
- 实现简单
- 无法延迟创建对象
懒汉式单例,线程不安全(LazyInitialization, Thread Not Safe)
现在我们把实例的初始化放到getInstance()方法里面去处理,这样就是懒汉式单例。
代码实现
|
|
这段代码实现起来也很简单,并且也使用了懒加载的模式,但是有一个比较严重的问题,如果有多个线程同时调用getInstance(),而此时恰好又没有instance,那么就会创建出多个实例,这样肯定是不符合我们的需要的,所以在多线程的条件下不能使用这种方法创建单例。
总结
- 实现简单
- 懒加载机制,可以延迟创建对象
- 多线程下不能工作
懒汉式单例,线程安全(Lazy Initialization, Thread Safe)
为了让我们的懒汉式单例在多线程的环境下使用,只需要把getInstance()方法设置为同步(synchronized)的即可。虽然这样解决了我们的问题,效率却非常的低,我们想要的是在第一次没有instance的情况对创建实例进行同步的操作,如果按照这种方式,getInstance()方法也只能有一个线程来操作,显然不太符合实际应用场景。
代码实现
|
|
总结
- 实现也比较简单
- 懒加载
- 多线程下能工作,效率低
双重检查锁单例(Double-Checked Locking)
在上面改进过的懒汉式单例中,效率太低的原因是我们把获取instance的方法也变成了同步操作,现在再改进一下,只把创建单例实例的方法设置成同步操作。
代码实现
|
|
现在我们在getInstance()方法里面会有两步操作,第一步判断是否有实例存在,如果没有实例就进入同步代码块,因为有可能会有多个线程同时进入同步代码块操作,所以在同步代码里面再对实例检查一遍是有必要的,否则有可能创建多个实例。改进过后只对创建实例进行同步操作,效率是提高了,但是新的问题又出现了。这行代码instance = new DoubleCheckedLockingSingleton();
看起来没什么问题,但是事实上并不是一次原子操作。这条语句实际在JVM中执行的时候干了以下几件事:
- 给instance对象分配内存空间
- 调用DoubleCheckedLockingSingleton()初始化instance对象
- 将instance对象指向已经分配的内存空间
但是JVM的编译器会对指令进行重排序的优化,有可能第3步在第2步之前已经执行过了,如果先执行的第3步,instance此时就不是null类型了,这时其他线程如果执行操作的话,就有可能把还没初始化的instance对象返回使用,这样就出问题了。
那么怎么办呢?只需要在声明instance对象时加上volatile关键字,代码如下。既保证了可见性,同时更重要的是禁止了指令重排序的优化。但是volatile只有在JDK5之后的版本避免重排序才是完全可用的。
|
|
总结
- 懒加载
- 性能尚可
- JDK5之后版本线程安全
静态内部类单例(BillPughSingleton)
由于这种方法是Bill Pugh提出来的,这种单例就以他的名字命名。这种写法实现原理是通过静态内部类来持有单例对象。代码如下
代码实现
|
|
总结
- 实现不复杂
- 懒加载方式
- 不需要同步操作就可以在多线程环境下运行
- 性能良好
- 不依赖于JDK版本
- 推荐使用:)
枚举(Enum)
还有一种特别的方式来写单例,就是使用枚举类型。这种写法十分简单,还可以用来生成多例模式,代码如下
代码实现
Enum单例模式
Enum多例模式
总结
- 实现特别简单
- 线程安全
- 防止由于序列化和反射对单例的破坏
- 不适合在Android中使用,参考Android Training文档(Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.)
单例序列化问题
使用我们的静态内部类进行序列化和反序列化操作之后,发现单例被破坏了。找到一种解决的办法,就是在实现类中提供readResolve()方法的实现。
代码实现
|
|
总结
本文分析了多种单例模式的实现方式以及实现原理,主要有懒汉式、饿汉式、双重检查锁、静态内部类、枚举这几种方式。在通常的情况下使用静态内部类的方式,当涉及到序列化和反射的情景时尽可能的使用枚举类型,但是在Android开发时避免使用Enum类型。
使用单例模式的优点
- 单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁创建销毁时,而且性能又无法优化,单例模式的优势就会非常明显。
- 单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象,则可以通过在应用启动时直接生成一个单例对象,然后用永久驻留内存的方式来解决。
- 单例模式可以避免对资源的多重占用。
- 单例模式可以在系统设置全局的访问点,优化和共享资源的访问。
使用单例模式的缺点
- 单例模式一般没有接口或者抽象,扩展非常困难,一般只能通过直接修改代码的方法来扩展实现。