Q:实现单例模式有几种方法?懒汉式中双层锁的目的是什么?两次判空的目的又是什么?
懒汉式(线程不安全)
单例模式最后的目的无非就是获取当前存在的实例对象,如果没有实例对象就实例化一个,有就直接返回。所谓懒汉式,可以从名字进行理解,就是在你第一次使用这个实例之前我都(懒得)不去进行实例化,一直等到第一次需要使用到这个单例的地方再(迫不得已)实例化。
提到单例模式,大多数开发者会直接写出下面的代码:
public class SingleTon {
private static SingleTon instance;
private SingleTon(){
}
public static SingleTon getInstance(){
if (instance==null){
instance = new SingleTon();
}
return instance;
}
}
而上面的代码也就是懒汉式的单例模式,只有在第一次调用了getInstance方法的地方才会对instance进行实例化。
这种单例模式的写法在单线程的时候没有问题,但需要注意的是,在 多线程的情况下,这种单例模式的写法可能会出现实例化多个单例的情况,即这种懒汉模式是线程不安全的,所以一般情况下也是不推荐这么写单例的。
懒汉式(线程安全)
像多数线程不安全的问题的解决方法相同,实现线程安全的懒汉单例进行加锁即可,例如直接将锁加在上面的例子里获取单例的静态方法getInstance上:
public static synchronized SingleTon getInstance(){
if (instance==null){
instance = new SingleTon();
}
return instance;
}
显然,这种方法可以解决线程不安全的问题,即使多线程并行也只会存在一个单例,但这种方法的缺点是效率太低,仔细想想,其实有必要进行加锁的只有第一次判断instance为空而进行实例化的时候,而在instance实例化完成后,后面无论是哪个线程读取到的instance都已经不为null,可以直接将当前instance返回。而这种对整个方法进行加锁的操作则会使得每一次获取单例都进行加锁,效率十分低下。
双重检验锁
针对上面提出的问题,出现了第三种单例模式的写法,即双重检验锁的单例模式:
public static SingleTon getInstance(){
if (instance==null){
synchronized (SingleTon.class){
if (instance==null){
instance = new SingleTon();
}
}
}
return instance;
}
所谓双层检验锁,即像上面的例子中所示的,在加锁前后对实例对象进行两次判空的检验,加锁是为了第一次对象实例化的线程同步,而锁内还要有第二层判空是因为可能会有多个线程进入第一层if判断内部,而在加锁代码块外排队等候,如果锁内不进行第二次检验,仍然会出现实例化多个对象的情况。
而进行了双重检验之后,除了第一次实例化需要进行加锁同步,之后的线程只要进行第一层的if判断不为空即可直接返回,而不用每一次获取单例都加锁同步,因此相比前面两种单例模式的写法,更推荐用这种双层检验锁的方式来写单例。
然而,双层检验锁也不是完美的,问题出现在对单例对象进行实例化的过程中,在执行instance = new SingleTon()这句代码时大概做了三件事:
- 给instance分配内存空间
- 通过调用SingleTon的构造函数初始化成员变量
- 将instance这个引用指向先前分配好的内存空间
JVM在执行编译的时候实际上不能保证上述三件事中2和3的执行顺序,也就是说可能发生的执行顺序是123或132,如果是123的执行顺序则不会发生问题,而如果是132的执行顺序,则instance作为一个引用,在指向分配好的内存空间的时候本身就已经不为null了,此时如果有另一条线程调用getInstance方法,则会直接将还没有对成员变量完成初始化的instance返回,程序就可能发生崩溃。
为了解决这个问题,我们加入了violate关键字:
private static violate SingleTon instance;
violate的作用可以理解为一个读写锁,使用violate修饰后,一旦开始对instance进行操作,则只能将前面所说的三步全部进行完之后才能再读取instance,有了violate关键字,才能解决上面提到的问题。
饿汉式
可以看到,前面说的所有问题的根源都出在多线程上,如果可以解决多线程同步的问题,那么前面的问题也就迎刃而解了,于是就出现了饿汉式的单例模式。
对比懒汉式,懒汉是懒得进行实例化,直到要用到这个单例的时候才进行实例化,那么饿汉就是饥饿难耐、等不及的就进行了实例化,直接来看代码:
public class SingleTon {
private static SingleTon instance = new SingleTon();
private SingleTon(){
}
public static SingleTon getInstance(){
return instance;
}
}
显然,由于被修饰为static变量,instance会在类第一次加载到内存中时就进行了实例化,由于类加载机制的特性,这种写法本身就是线程安全的,所以不会有前面三种方式中存在的问题。
由于static修饰,instance会在类第一次被加载到内存中时就被初始化,如果某次运行期间并没有用到这个单例,就浪费了资源去加载了这个对象,不能做到延时加载;另一方面,有些时候我们需要依赖其他类创建的资源来创建这个单例,即需要在我们第一次调用getInstance方法的时候才真正的创建单例对象,这种一开始就初始化完成的方式也无法满足这种需求。
静态内部类式
针对上面饿汉式方法存在的问题,能不能有一种既能延迟加载、又能解决同步问题的单例模式写法呢?这便需要用静态内部类的方式了。
public class SingleTon {
private static class SingleTonInstance{
private static SingleTon instance = new SingleTon();
}
private SingleTon(){
}
public static SingleTon getInstance(){
return SingleTonInstance.instance;
}
}
由于静态内部类是私有的,因此除了getInstance方法没有其他方式能访问到它,因此只有在getInstance调用时才能真正创建单例,实现了单例创建的延迟,同时由于静态类只会加载一次的特性,因此也不会出现线程同步问题
- 都需要额外的工作实现序列化,否则每次反序列化一个序列化对象时都会创建一个新的实例;
- 仍然可以使用反射强行调用私有的构造函数构建单例。
枚举
实际上,在Java中最优雅的单例写法是使用枚举。
public enum EnumSingle {
INSTANCE;
}
简单的一个枚举,实际上就解决了线程同步、防止反射调用构造函数的问题,另一方面,枚举提供了自动序列化机制,防止了反序列化的时候重新创建对象,因此这是新版effective Java最推荐的单例写法。
注意
前面说枚举是最推荐的单例写法,从理论上来说,同时解决了前面所说的同步问题、反射调用构造函数问题、反序列化重新创建对象问题的枚举写法确实是最优雅的单例模式,但是需要注意的是,枚举类并不是普通类,使用枚举类对象作为单例自然丢弃了一些普通类的特性(继承等),而且枚举和饿汉式写法一样,无法实现延迟加载。
所以综上所述,如果不需要延迟加载,可以优先使用枚举实现单例;如果需要延迟加载,可以优先使用静态内部类。
当然,上面的分析是针对Java来说的,但是实际上在Android中因为性能问题不推荐使用枚举,而且Android中使用的Java都是在1.5版本以上的,所以Android使用的单例推荐使用静态内部类或者双重检验锁。