当看完《Head First Design Patterns》一书之后,你不一定记得住所有的设计模式,你需要在阅读或编写代码的过程中循序渐进地掌握每一种设计模式,做到所谓的各个击破。本文笔者就先来把Singleton这颗蛋吃掉。

徒手写一个线程不安全的Singleton:

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {}  //  私有化构造函数,使其不能外部实例化

    public static getInstance() {   // 好吧,说实话,徒手写忘了加static
        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }
}

上面的 getInstance 可以被多个线程访问,因此不难理解各个线程可能获得不同的对象,违背了单例的原则。

然后为了避免上面的情况,我们可以使用 synchronized 来迫使每个线程在进入这个方法之前先等候其他线程离开,于是有了单例的第二个实现:

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {}  //  私有化构造函数,使其不能外部实例化

    public static synchronized getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }
}

但是这个代码又有问题了,每次获取实例都需要等待其它线程的离开,极大影响性能。单例初始化完成之后,之后的同步等待就没有必要了。因此我们使用双重检查加锁的方式来写第三个版本的单例模式:

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {}  //  私有化构造函数,使其不能外部实例化

    public static getInstance() {   // 不再同步整个函数
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }

        return instance;
    }
}

但是由于编译器的指令重排,上述代码依然不是绝对线程安全的。指令重排什么意思呢?通俗的讲,Java编译器会将 instance = new Singleton() 指令编译成如下JVM指令:

memory = allocate();    // 分配内存
ctorInstance(memory);   // 初始化对象
instance = memory;      // 设置内存指向

但是由于指令重排,上述指令的顺序可能变成了:

memory = allocate();    // 分配内存
instance = memory;      // 设置内存指向
ctorInstance(memory);   // 初始化对象

这样当在第二个线程判断 if (instance == null) 返回 false 的时候,上述的第三个指令可能还未被第一个线程执行,因此第二个线程获取的 instance 对象是未初始化的。

解决这个问题需要使用 volatile 指令,于是有了最终版的线程安全的单例模式:

public class Singleton {
    private volatile static Singleton instance = null;  // 禁止指令重排

    private Singleton() {}  //  私有化构造函数,使其不能外部实例化

    public static getInstance() {   // 不再同步整个函数
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }

        return instance;
    }
}

以上写法属于懒汉模式,还有一种饿汉模式,即单例对象一开始就被 new Singleton() 主动构建,利用这个做法,我们依赖 JVM 在加载这个类时马上创建唯一的单例实例。JVM 保证任何线程访问 instance 静态变量之前,一定先创建此实例。

public class Singleton {
    private static Singleton intance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return intance;
    }
}

现实中我们会遇到很多饿汉模式的单例实现,如 Runtime 类:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}

    // below are other useful methods
}

ok,以上就是我们面对大多数面试官时你需要写出单例模式。但是单例模式存在一个问题: 无法防止通过反射来重复构建对象:

// 获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
// 设置可访问
con.setAccessible(true);
// 构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();

那么如何实现一个可以防止反射的单例模式呢?可以使用枚举实现单例,这是一种优雅而又简洁的方式:

public enum SingletonEnum {
    INSTANCE;
}

枚举的方式笔者了解的还不透彻,等掌握了再来补充。