引言
单例模式,这个名字大家绝对是耳熟能详。作为对象的创建模式,单例模式实质上是为了确保某一个类它只有一个实例,并且自行实例化向整个系统提供这个实例。
单例模式的要点
单例模式的要点有三个,这也是我们在设计单例模式时需要注意的几点:
- 单例类在整个系统的运行过程中只能有一个实例;
- 单例类必须要自行来创建实例(换句话说,在Java中就是不对外暴露构造方法,不能由其他的对象来做实例化,除了自己);
- 单例类必须自行向这个系统提供这个实例。
单例模式的实现
饿汉式单例类
饿汉式单例类是实现起来最为简单的单例类,如下类图所示:
代码如下:
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
可以看出,在在这个类被加载时,静态变量instance会被初始化,这时候类的私有构造方法会被调用,单例类的唯一实例被创建。
需要注意的是:由于构造方法是私有的,单例类不能是不能被继承的。
懒汉式单例类
先看看类图:
代码如下:
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
public synchronized static LazySingleton getInstance() {
if (null == instance) {
instance = new LazySingleton();
}
return instance;
}
}
从代码可以看出,跟饿汉式不一样的是不是在类被加载的时候就实例化,而是在单例类第一次被引用的时候再实例化。上述代码还稍微做了一写措施来保证线程安全。
到这里已经给出了两种单例实现的模式,但是细心的读者可能都会发现,这两种方式对并发的处理并不是那么的好,单例模式最需要注意的就是线程安全的问题,因为全程可能有n个对象都在修改单例类对象。
之前有了解过单例模式或者看过博客的读者可能很容易就想到double check,双重检查,接下来我就懒汉模式从线程不安全到线程安全举例加以说明,同时说说为什么我觉得双重检查是错误的,做不到线程安全。
线程安全的单例
首先考虑单线程版本的:
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (null == instance) {
instance = new LazySingleton();
}
return instance;
}
}
很明显,在多线程的环境下,这样子写你的系统肯定game over了。
我给具体分析下为什么这样写是错误的:
假设现在有A和B两个线程几乎同时到底if(null == instance),假设A比B早那么一点点,那么:
- A首先进入if(null == instance)代码块内部,并且开始执行new LazySingleton()语句,此时,instance的值仍然为null,直到赋值语句执行完毕;
- 线程B不会在if(null == instance)语句外等待,此时还未赋值给instance,if语句成立,它会马上进入if代码块内部,B也开始执行instance = new LazySingleton()语句,创建出第二个实例;
- A的instance = new LazySingleton()执行完毕后,这时候instance不为null了,第三个线程不会再进入到if代码块内部;
- B也创建了一个实例,instance的变量值被覆盖,但是A引用的之前的instance不会被改变。这时候A和B都各自拥有一个独立的instance对象,这是不对的。
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
public synchronized static LazySingleton getInstance() {
if (null == instance) {
instance = new LazySingleton();
}
return instance;
}
}
加了synchronized关键字之后,这个静态工厂方法都是同步的,当线程A和B同时调用此方法时:
- 早到一点点的线程A率先进入此方法,B在方法外部等待;
- A执行instance = new LazySingleton(),创建出实例;
- 方法锁释放,线程B进入此方法,此时instance不再为null,if代码块不会再次被执行。线程B取到的instance变量所含有的引用与A是同一个。
先看看双重检查代码:
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (null == instance) { //第一次检查,位置1
//多个线程同时到达,位置2
synchronized(LazySingleton.class) {
//这里只能有一个线程,位置3
if (null == instance) {//第二次检查,位置4
instance = new LazySingleton();
}
}
}
return instance;
}
}
同样还是假设线程A和B几乎同事调用静态工厂方法:
- 因为A和B是一批调用者,因此当它们进入此静态方法工厂时,instance变量为null,线程A和B几乎同时到达位置1;
- 假设A先到达位置2,进入synchronized(LazySingleton.class)到达位置3,这是,由于synchronized(LazySingleton.class)的同步限制,B无法到达位置3,只能在位置2等候;
- A执行instance = new LazySingleton()语句,实例化instance,此时B还在位置2处等候;
- A退出synchronized(LazySingleton.class),返回instance,退出静态工厂方法;
- B进入synchronized(LazySingleton.class)块,到达位置3,并且到达位置4,这时instance已不为空,B退出synchronized(LazySingleton.class),返回A创建的instance,退出工厂方法。此时A和B得到的是同一个instance对象。
双重检查成例对Java语言编译器不成立
在Java编译器中,LazySingleton类的初始化和instance变量赋值的顺序是不可预料的,如果一个线程在没有同步化的条件下读取instance的引用,并且调用这个对象的方法的话,可能会发现对象的初始化过程尚未完成,从而造成崩溃。
在一般情况下,饿汉模式+懒汉模式基本上足以解决在实际设计工作中遇到的问题,建议读者不要过分在双重检查上面花费太多时间。