概念
单例模式,又称单件模式或者单子模式,指的是一个类只有一个实例,并且提供一个全局访问点。
实现思路
- 在单例的类中设置一个private静态变量sInstance,sInstance类型为当前类,用来持有单例唯一的实例。
- 将(无参数)构造器设置为private,避免外部使用new构造多个实例。
- 提供一个public的静态方法,如getInstance,用来返回该类的唯一实例sInstance。
其中上面的单例的实例可以有以下几种创建形式,每一种实现都需要保证实例的唯一性。
饿汉式
饿汉式指的是单例的实例在类装载时进行创建,根据JLS(Java Language Specification)中的规定,一个类在一个ClassLoader中只会被初始化一次,这点是JVM本身保证的,所以你不用担心多线程的问题。如果单例类的构造方法中没有包含过多的操作处理,饿汉式其实是可以接受的。
饿汉式的常见代码如下,当SingleInstance类加载时会执行private static SingleInstance sInstance = new SingleInstance();
初始化了唯一的实例,然后getInstance()
直接返回sInstance即可。
public class SingleInstance {
private static SingleInstance sInstance = new SingleInstance();
private SingleInstance() {
}
public static SingleInstance getInstance() {
return sInstance;
}
}
饿汉式的问题
- 如果构造方法中存在过多的处理,会导致加载这个类时比较慢,可能引起性能问题。
- 如果使用饿汉式的话,只进行了类的装载,并没有实质的调用,会造成资源的浪费。
- 如果创建对象的时候依赖某些参数或配置文件时,在getInstance()之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
懒汉式
懒汉式指的是单例实例在第一次使用时进行创建。这种情况下避免了上面饿汉式可能遇到的问题。但是考虑到多线程的并发操作,我们不能简简单单得像下面代码实现。
public class SingleInstance {
private static SingleInstance sInstance;
private SingleInstance() {
}
public static SingleInstance getInstance() {
if (null == sInstance) {
sInstance = new SingleInstance();
}
return sInstance;
}
}
上述的代码在多个线程密集调用getInstance时,存在创建多个实例的可能。比如线程A进入null == sInstance这段代码块,而在A线程未创建完成实例时,如果线程B也进入了该代码块,必然会造成两个实例的产生。
synchronized修饰方法
使用synchrnozed修饰getInstance方法可能是最简单的一个保证多线程保证单例唯一性的方法。synchronized修饰的方法后,当某个线程进入调用这个方法,该线程只有当其他线程离开当前方法后才会进入该方法。所以可以保证getInstance在任何时候只有一个线程进入。
public class SingleInstance {
private static SingleInstance sInstance;
private SingleInstance() {
}
public static synchronized SingleInstance getInstance() {
if (null == sInstance) {
sInstance = new SingleInstance();
}
return sInstance;
}
}
但是使用synchronized修饰getInstance方法后必然会导致性能下降,而且getInstance是一个被频繁调用的方法,除了第一次调用时是执行了SingleInstance的构造函数之外,以后的每一次调用都是直接返回instance对象。返回对象这个操作耗时是很小的,绝大部分的耗时都用在synchronized修饰符的同步准备上,因此从性能上说很不划算。虽然这种方法能解决问题,但是不推荐。
双重检查加锁(DCL)
使用双重检查加锁(double checked locking,DCL),首先进入该方法时进行null == sInstance检查,如果第一次检查通过,即没有实例创建,则进入synchronized控制的同步块,并再次检查实例是否创建,如果仍未创建,则创建该实例。
双重检查加锁保证了多线程下只创建一个实例,并且加锁代码块只在实例创建的之前进行同步。如果实例已经创建后,进入该方法,则不会执行到同步块的代码。
public class SingleInstance {
private static SingleInstance sInstance;
private SingleInstance() {
}
public static SingleInstance getInstance() {
if (null == sInstance) { //当对象创建后,每次进来都可以直接返回,不必再进入同步代码块
synchronized (SingleInstance.class) {
if (null == sInstance) {
sInstance = new SingleInstance();
}
}
}
return sInstance;
}
}
同步块中判断null == sInstance的作用是,当两个线程A、B同时进入getInstance()方法后,并且符合了第一个非null判断,当线程A在创建对象离开同步块后,随即线程B进入同步代码块,如果此时没有进行非null判断,就会再次创建对象。
volatile双重检查加锁
上面的例子已经完美了吗?不,我们来看看这个场景:假设线程一执行到sInstance = new SingleInstance();这句,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情:
- 给SingleInstance 的实例分配内存。
- 初始化SingleInstance的构造器
- 将sInstance对象指向分配的内存空间(注意到这步instance就非null了)。
但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候sInstance因为已经在线程一内执行过了第三点,sInstance已经是非空了,所以线程二直接拿走sInstance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来,真是一茶几的杯具啊。
DCL的写法来实现单例是很多技术书、教科书(包括基于JDK1.4以前版本的书籍)上推荐的写法,实际上是不完全正确的。的确在一些语言(譬如C语言)上DCL是可行的,取决于是否能保证2、3步的顺序。在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字,因此如果JDK是1.5或之后的版本,只需要将sInstance的定义改成“private volatile static SingleInstance sInstance null;”就可以保证每次sinstance都从主内存读取,就可以使用DCL的写法来完成单例模式。当然volatile或多或少也会影响到性能,最重要的是我们还要考虑JDK1.42以及之前的版本。
public class SingleInstance {
private static volatile SingleInstance sInstance;//volatile修饰
private SingleInstance() {
}
public static SingleInstance getInstance() {
if (null == sInstance) {
synchronized (SingleInstance.class) {
if (null == sInstance) {
sInstance = new SingleInstance();
}
}
}
return sInstance;
}
}
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。使用volatile修饰sInstance变量之后,可以确保多个线程之间正确处理sInstance变量。关于volatile,可以访问深入分析Volatile的实现原理了解更多。
静态类方式
在Java中,类的静态初始化会在类被加载时触发,我们利用这个原理,可以实现利用这一特性,结合内部类,可以实现如下的代码,进行懒汉式创建实例。
public class SingleInstance {
private SingleInstance() {
}
public static SingleInstance getInstance() {
return SingleInstanceHolder.sInstance;
}
private static class SingleInstanceHolder {
private static SingleInstance sInstance = new SingleInstance();
}
}
这种写法仍然使用JVM本身机制保证了线程安全问题;由于SingleInstanceHolder 是私有的,除了getInstance()之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖JDK版本。
基于volatile的双重检查锁定的方案和基于类初始化的方案,我们会发现基于类初始化的方案的实现代码更简洁。但基于volatile的双重检查锁定的方案有一个额外的优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。
枚举方式
DCL也可能会创建不止一个实例,尽管在Java5这个问题修复了(jdk1.5在内存模型上做了大量的改善,提供了volatile关键字来修饰变量),但是仍然对新手来说还是比较棘手。对比通过double checked locking 实现同步,枚举单例那实在是太简单了。
枚举enum是java5引入的,我们可以利用枚举实现单例模式。枚举实现起来简单,线程安全(默认枚举实例的创建是线程安全的,由jvm保证的),只有一个实例(Java 通过编译器和 JVM 联手来防止enum 产生超过一个class:不能利用 new、clone()、de-serialization、以及 Reflection API 来产生enum 的 instance)。
通过enum关键字来实现枚举,在枚举中需要注意的有:
1. 枚举中的属性必须放在最前面,一般使用大写字母表示
2. 枚举中可以和java类一样定义方法
3. 枚举中的构造方法必须是私有的
public enum Singleton{
INSTANCE;
private Singleton(){
}
}
枚举单例有序列化和线程安全的保证,而且只要几行代码就能实现是单例最好的的实现方式,不过你仍然可以使用其它的方式来实现单例,但是我仍然得不到一个更有信服力的原因不去使用枚举。
常见创建对象的几种方式
其实,单例模式并不能保证实例的唯一性,只要我们想办法的话,还是可以打破这种唯一性的。以下几种方法都能实现。
使用反射,虽然构造器为非公开,但是在反射面前就不起作用了,可以使用setAccessible方法来突破private的限制。
如果单例的类实现了cloneable,那么还是可以拷贝出多个实例的。
通过序列化构造单例对象。传统单例存在的另外一个问题是一旦你实现了序列化接口,那么它们不再保持单例了,因为readObject()方法一直返回一个新的对象就像java的构造方法一样,你可以通过使用readResolve()方法来避免此事发生,看下面的例子:
public class SingleInstance {
private SingleInstance() {
}
public static SingleInstance getInstance() {
return SingleInstanceHolder.sInstance;
}
private static class SingleInstanceHolder {
private static SingleInstance sInstance = new SingleInstance();
}
/**
* readResolve方法应对单例对象被序列化时候
*/
private Object readResolve() {
return getInstance();
}
}
这样甚至还可以更复杂,如果你的单例类维持了其他对象的状态的话,因此你需要使他们成为transient的对象。但是枚举单例,JVM对序列化有保证。
- 使用多个类加载器加载单例类,也会导致创建多个实例并存的问题。
除了第四项,枚举单例都可以保证只有一个实例,可以说是个值得使用的单例子方式。
各种单例模式对比
模式 | 优点 | 缺点 |
---|---|---|
饿汉式 | 1.线程安全 2.类加载后就已经创建好对象,调用时反应速度快 | 1.灵活性欠佳。创建对象时,如果需要依赖某些参数 ,就无法使用。 2.资源效率不高。如果没有执行getInstance()方法但却调用了该类的静态方法或字段,造成类加载,就会使对象被实例化。 3.可以通过反射、clone、序列化等方式创建新的对象 |
volatile双重检查加锁 | 1.线程安全(java5上) 2.灵活性高,创建对象时,可以依赖参数配置 | 1.java5前存在线程不安全问题 2.书写繁琐 3.可以通过反射、clone、序列化等方式创建新的对象 |
静态内部类 | 1.线程安全 2.灵活性高 | 1.可以通过反射、clone、序列化等方式创建新的对象 |
枚举 | 1.线程安全 2.书写简便 3.无法通过反射、clone、序列化等方式创建新的对象 | 暂无 |
总结
其他单例模式的写法还有很多,如使用本地线程(ThreadLocal)来处理并发以及保证一个线程内一个单例的实现、GoF原始例子中使用注册方式应对单例类需要需要继承时的实现、使用指定类加载器去应对多ClassLoader环境下的实现等等。我们做开发设计工作的时,应当既要考虑到需求可能出现的扩展与变化,也应当避免“幻影需求”导致无谓的提升设计、实现复杂度,最终反而带来工期、性能和稳定性的损失。设计不足与设计过度都是危害,所以说没有最好的单例模式,只有最合适的单例模式。
来自
单例模式中为什么用枚举更好
双重检查锁定与延迟初始化
单例这种设计模式
探索设计模式之六——单例模式
10个单例子模式的面试问题