单例模式是GoF23种最常用的设计模式之一,也是经常被面试问到的问题。单例模式提供了一种在多线程情况下保证实例唯一性的解决方案。单例模式实现虽然简单,但是实现方式却多种多样,本文从线程安全、高性能、懒加载三个维度逐一分析。
1、饿汉模式
public class Singleton{
private static Singleton instance = new Singleton();
private Singleton () {
}
public static Singleton getInstance() {
return instance;
}
}
饿汉模式最为简单粗暴,将instance作为静态类变量案,使得instance在类初始化时得到实例化,百分百保证同步。但由于实例化的对象,可能很长一段时间才会被使用到,也就是使得该对象长久的占据堆内存而未被使用,所以除非单例对象的类成员不多,且在程序运行早起就被使用,否则不推荐该方法。
2、懒汉模式
public class Singleton{
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if (instance == null) {
instance = new Singleton();
}
return instance;
}}
相比于饿汉模式,懒汉模式的区别在于instance并未一开始就实例化,而是在被使用的时候才实例化,这样就避免了类在初始化时就提前创建了对象。但这样使得在多线程的情况下,不能保证对象只被创建一次,也就不能保证单例的唯一性。
3、懒汉+同步方法
public class Singleton{
private static Singleton instance;
private Singleton(){}
public static synchronized Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}}
懒汉模式可以保证实例的懒加载,但无法保证实例的唯一性,为了解决这个问题引申出了懒汉同步的方式,即加上 synchronized 关键字,在创建对象的时候,做同步约束。但是,懒汉+同步的方式,虽然解决做到了懒加载,同时也做了同步约束,但由于synchronized关键字的排他性,使得getInstance()方法性能下降了。
4、双重校验锁
public class Singleton {
private static Singleton singleton;
private Connection conn;
private Socket socket;
private Singleton () {
this.conn = //初始化
this.socket = //初始化
}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
鉴于懒汉+同步的方式,依然不能满足我们的要求,再次基础上继续优化,出现了双重校验锁的方式。依然需要判断singleton是否为空,只是区别在于singleton作用的对象不再是getSingleton()方法,而是作用于整个单例类,两次校验singleton,但并不阻塞getSingleton()方法。
双重校验锁的方式看似完美,但有一个致命的缺陷:在Singleton的构造函数中,需要分别初始化conn和socket,但是根绝JVM指定重排序和Happens-Before规则,singleton、conn、socket三者之间的实例化顺序无前后约束的关系,就可能是singleton初始化完成,而conn、socket未被初始化完成,而如果这个时候singleton被调用,使用了conn、socket其中之一,则会导致 空指针异常
5、volatile+双重校验锁
public class Singleton {
private volatile static Singleton singleton;
private Singleton () {
}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
可以看到,在双重校验锁的基础上,加上volatile关键字,防止JVM在运行时的指定重排序,就避免了双重校验锁可能导致的空指针异常*
6、Holder——静态内部类
public class Singleton {
private static class SingletonHolder{
private static final Singleton INSTSNCE = new Singleton();
}
private Singleton () {}
public static final Singleton getInstance () {
return SingletonHolder.INSTANCE;
}
}
Holder方式完全借助了类加载的特点,在Singleton类中并没有上面列举的方法那样存在静态成员,而是将其放到了静态内部类SingletonHolder中,因此在Singleton类的初始化过程中并不会创建Singleton的实例。SingletonHolder类中定义了Singleton的静态变量并直接进行了实例化,当SingletonHolder被引用的时候才会创建Singleton的实例。根据类加载机制的特点,这样能保证Singleton不会提前被创建,同时又保证安全。
7、枚举
public class Singleton {
INSTANCE;
Singleton () {}
public static void whateverMethod() {
}
public static Singleton getInstance () {
return INSTANCE;
}
}
根据枚举的特性,枚举类型不允许被继承,同样是线程安全且只能被实例化一次,但是枚举不能够做到懒加载。
即便如此,我们可以利用Holder的方式对枚举模式进行改造,将静态内部类改为内部枚举,改法可以自行摸索,不再举例说明。
Holder和枚举方式的单例设计是最好的设计,这种两种方式都是Effective Java作者Josh Bloch 提倡的方式,它们不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
Q.E.D.