作为最常用的设计模式——单例模式,是平常最常用的,也是面试最常问的。在上一节中讲到了对象的发布与逃逸,如何安全的发布对象?单例模式给出了答案。单例模式的应用非常广,在连接池,线程池,以及Spring框架中的所有类,都是单例模式的体现
饿汉式
单例模式也就是说类只有一个实例,那么构造方法肯定是私有的,提供一个工厂方法来创建对象。而饿汉式则是无论是否需要该实例,在类创建的时候就初始化该实例,当需要的时候返回即可
直接初始化
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return singleton;
}
}
在一开始就创建了实例singleton,当该实例的时候,直接通过getinstance方法来获得即可。那么如何保证多线程下,该类只有一个实例?在第一次使用类的时候,会进行加载,验证,准备,解析,初始化五个阶段,在准备阶段,会为静态变量分配空间,在初始化阶段,会为静态变量赋值,该过程由jvm完成,并且保证了该过程只会执行一次,所以在多线程下,该写法是安全的
静态代码块
和上面的思想类似,在初始化阶段不但为静态变量赋值,还会执行静态代码块,所以,我们也可也将初始化的过程挪到静态代码块中
class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
static {
singleton = new Singleton();
}
public static Singleton getInstance() {
return singleton;
}
}
这两种写法都是饿汉式的写法,思考一下不难发现,这种写法不论是否调用getInstance方法,初始化的工作都会完成,这种写法与懒加载的写法比起来,有可能就会出现一定的浪费,下面看一下懒汉式的写法
懒汉式
懒汉式顾名思义,就是只有当第一次外界需要拿到实例的时候,才会去初始化实例,并返回给调用对象,那么很多人会说,这很简单啊, 这样写即可:
class Singleton {
private static Singleton singleton = null;
private Singleton() {}
public static Singleton getInstance() {
//如果单例没有初始化,那么直接初始化,再进行返回
//如果初始化过了,那么直接返回
//位置1
if(singleton == null) {
//位置2
singleton = new Singleton();
}
return singleton;
}
}
这种写法看似没有问题,但是如果在多线程的情况下,线程1走到了位置2,但是这个时候还没有执行初始化语句,线程2已经到位置1进行了判断,然后线程2也走到了位置2,那么两个线程会执行两次初始化语句,调用两次构造函数,这里的构造虽然为空,只是作为演示,实际的过程中构造函数会做很多的事情如资源的初始化等等,那么这种写法肯定是线程不安全的
既然是线程不安全的,在Java中可以使用synchronized来保证线程安全,于是就想到了这种写法:
class Singleton {
private static Singleton singleton = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if(singleton == null) {
singleton = new Singleton3();
}
return singleton;
}
}
通过锁住静态方法也就是锁住整个类,来保证线程安全,现在安全解决了,但是却带来了很大的效率问题,不管是第几次访问该方法,都会进行加锁,解锁的过程,这样的话初始化完成后,后面每次去获取实例的时候都是串行执行,效率很低,这就出现了双重校验锁的方法
class Singleton {
private static Singleton singleton = null;
private Singleton() {}
public static Singleton getInstance() {
//先判断有没有必要加锁
if(singleton == null) {
synchronized (Singleton.class) {
if(singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
但是这种写法是线程不安全的?为什么呢?在进行初始化对象的时候分为三步:
(1)memory = allocate() 分配对象的内存空间,对所有的实例变量赋默认值
(2)ctorInstance() 初始化对象
(3)instance = memory 设置instance指向刚分配的内存
在jvm底层,会进行指令的重排序,(2)和(3)会进行重排序,实际的执行顺序是(1)(3)(2),那么如果线程1已经执行到了(3),但是并没有初始化对象,这时实例并不为null,线程2过来进行第一重判断后,直接就将还没有初始化的对象进行了返回,造成了不安全的情况。
可以通过volatile关键字来保证,volatile的作用由两个:1.保证可见性,2.防止执行重排序。为singleton加上volatile即可:
private volatile static Singleton singleton = null;
通过枚举实现
public class SingletonExample {
private SingletonExample() {
}
public static SingletonExample getInstance() {
return Singleton.INSTANCE.getInstance();
}
private enum Singleton {
INSTANCE;
private SingletonExample singleton;
Singleton() {
singleton = new SingletonExample();
}
public SingletonExample getInstance() {
return singleton;
}
}
}
由于枚举类的特殊性,枚举类的构造函数Singleton方法只会被实例化一次,且是这个类被调用之前。这个是JVM保证的。对比懒汉与饿汉模式,它的优势很明显。