[design pattern]话说单例模式 – Singleton - 20110621更新
说到设计模式,最简单的可能要数单例模式 – Singleton。
但是在不同的应用场景和要求下,单例模式视乎并未像看起来的那么简单。
这篇文章旨在总结在Java语言中实现单例的各种情况。
定义: [GoF 95]
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
目录:
- 最简单的实现,直接看代码。
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }构造器私有确保外部不能实例化对象(真实环境中一般用protected修饰以方便reuse和单元测试),私有静态变量instance保存唯一一个实例,这两条实现了单例模式定义中的第一点。
静态公共方法getInstance()提供了全局访问点,实现了定义的第二点。 - 延迟初始化 – Lazy Initialization
对于某些应用,单例可能是一个很庞大的对象,需要占用大量的资源。所以希望在没用到这个单例之前都不去实例化它,直到第一次用到时再创建这个单例。public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } }以上两种是最基本的单例实现方法。
主要区别在于单例的初始化时机,第一种在JVM通过ClassLoader加载Singleton类时的initialization阶段初始化,也称之为饿汉式,第二种在第一次调用getInstance方法时初始化,也称之为懒汉式。 - 带同步的懒汉式
对于懒汉式,如果在多线程环境下,在instance的初始化时(第8行)可能会出现多个实例。
考虑两个线程:- 线程1执行到getInstance方法的第7行,此时instance为null
- 这时候CPU切换到线程2,线程2也执行到第7行,instance仍为null
- CPU切换回线程1,new Singleton();创建单例实例,并返回instance,退出方法
- CPU切换回线程2,因为线程2在上次获得CPU时已经判断了instance为null,所以它也开始执行new Singleton();实例化单例实例(尽管这时候instance实际上已经有了实例),结果导致程序中有2个不同的Singleton对象存在
对于饿汉式,不存在这个问题,这是由JVM保证的。
同一个Class在同一个命名空间下只会被JVM加载一次,所以instance会在这唯一一次的加载过程中初始化(具体是在类加载的initialization阶段初始化的),此时不存在多线程并发访问的问题。对于这个多线程问题,最直接的办法就是给getInstance方法加synchronized关键字
public static synchronized Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; }这样保证了多线程并发初始化的问题,但是却使得每次获取单例实例的时候都需要同步(其实,只有在instance实例化的时候才有并发出问题的可能,其他情况下,多线程只是获取单例引用,这时候完全没有必要同步)
- 两次检查加锁 – Double Check Lock
所以我们考虑只在instance实例化的时候才加锁,instance生成后的引用获取部分不再同步public class Singleton { /* 保存单例实例的变量需要添加volatile修饰符 */ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance(){ //先检查实例是否存在,如果不存在才进入下面的同步块 if(instance == null){ //同步块,线程安全的创建实例 synchronized(Singleton.class){ //再次检查实例是否存在,如果不存在才真的创建实例 if(instance == null){ instance = new Singleton(); } } } return instance; } }可以看到getInstance没有在整个方法上进行同步,同步块位于instance为null的情况下,所以只有在instance要进行首次实例化时由于多线程竞争才进行了同步。而当单例实例正常实例化后,以后的getInstance调用都直接return引用,不会再有同步消耗。很明显,这种方法要比前面的同步方法高效得多。
另外要注意的一点是单例实例instance要用volatile修饰符修饰,并且要使用JDK1.5以上版本。如果使用JDK1.4及以前版本,即使加了volatile关键字,仍然会存在下面这个问题:
由于1.5之前Java的内存和多线程模型会对字节码指令进行重排,可能会导致给instance引用赋值早于Singleton对象的初始化。因此,某个线程可能会看到instance不为空,但是instance对应的单例实例还未初始化好,这个对象的fields都是默认值,而不是应该初始化后得到的值。
JDK1.5以后扩展了volatile原本的语义,保证被其修饰的变量的写操作不会被指令重排。
想了解更多细节,参考这两篇文章:
The “Double-Checked Locking is Broken” Declaration
Synchronization and the Java Memory Model - 静态内部类
public class Singleton { private static class SingletonHolder { public static Singleton instance = new Singleton(); } private Singleton() { } public static Singleton getInstance() { return SingletonHolder.instance; } }前面我们想解决的问题主要有两个:
一是希望单例实例能够延迟初始化,另外一个就是确保多线程情况下也不会有问题。通过静态内部类的静态实例变量来保存单例实例就可以完美的解决这两个问题:
静态内部类是类级别的内部类,该内部类的实例与外部类的实例没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载;而instance在静态内部类里,作为静态实例采用饿汉式初始化,由JVM保证了线程安全。PS:一般认为是University of Maryland的Bill Pugh最早写出这种方法。
- 防止通过反射和解序列生成多个实例
单例模式的一个关键要素是“保证一个类仅有一个实例”,但是通过反射API,即使是private构造器,在外部仍然可以创建出新的实例。Class singletonClass = Singleton.class; Constructor cons = singletonClass.getDeclaredConstructor(null); cons.setAccessible(true); Singleton s1 = (Singleton)cons.newInstance(null); Singleton s2 = (Singleton)cons.newInstance(null);
对于这个问题,一种办法是通过SecurityManager控制反射的权限,另外也可以在程序设计上进行一些限制,比如执行构造器时如果已有单例实例的情况下抛出异常,或者采用抽象单例类,而在getInstance方法内部用匿名内部子类实例化单例返回。
另外,如果这个单例类是可序列化的,通过解序列也可以生成多个实例。
关于breaking singleton的更多详细内容,可以参考这篇文章
- 枚举
public enum Singleton { uniqueInstance; //单例的功能方法 public void operation1(){ //实现... } }从源码可以看到,使用枚举实现单例非常简洁,由JVM从根本上提供了单实例保障,绝对防止多次实例化,不存在多线程初始化问题,而且不支持反射。另外,还免费提供了序列化机制。可以说是更简洁、高效、安全的实现方式。
当然,这种方式也有缺点:不能继承,没法实现多态单例,不再能扩展成2例/3例/任意例。
related post
- pentadactyl试用记
- 仅保持一个firefox窗口 for firefox4
- [KeySnail]编辑模式下使用可输入字符作为快捷键
- [uc]sideBookmarkBar for firefox4
- Lock and Protect App Tab - 锁定和保护App Tab