单例怎么写( 四 )


if (s == null) {
s = new Singleton();
}
return s;
}
}
7. 如何正确地写出单例模式 当被问到要实现一个单例模式时 , 很多人的第一反应是写出如下的代码 , 包括教科书上也是这样教我们的 。
1234567891011public class Singleton {private static Singleton instance;private Singleton (){}public static Singleton getInstance () {if (instance == null ) {instance = new Singleton();}return instance;}}这段代码简单明了 , 而且使用了懒加载模式 , 但是却存在致命的问题 。当有多个线程并行调用 getInstance() 的时候 , 就会创建多个实例 。
也就是说在多线程下不能正常工作 。懒汉式 , 线程安全为了解决上面的问题 , 最简单的方法是将整个 getInstance() 方法设为同步(synchronized) 。
123456public static synchronized Singleton getInstance () {if (instance == null ) {instance = new Singleton();}return instance;}虽然做到了线程安全 , 并且解决了多实例的问题 , 但是它并不高效 。因为在任何时候只能有一个线程调用 getInstance() 方法 。
但是同步操作只需要在第一次调用时才被需要 , 即第一次创建单例实例对象时 。这就引出了双重检验锁 。
双重检验锁双重检验锁模式(double checked locking pattern) , 是一种使用同步块加锁的方法 。程序员称其为双重检查锁 , 因为会有两次检查 instance == null  , 一次是在同步块外 , 一次是在同步块内 。
为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if , 如果在同步块内不进行二次检验的话就会生成多个实例了 。12345678910public static Singleton getSingleton () {if (instance == null ) { //Single Checkedsynchronized (Singleton.class) {if (instance == null ) { //Double Checkedinstance = new Singleton();}}}return instance ;}这段代码看起来很完美 , 很可惜 , 它是有问题 。
主要在于 instance = new Singleton() 这句 , 这并非是一个原子操作 , 事实上在 JVM 中这句话大概做了下面 3 件事情 。给 instance 分配内存调用 Singleton 的构造函数来初始化成员变量将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)但是在 JVM 的即时编译器中存在指令重排序的优化 。
也就是说上面的第二步和第三步的顺序是不能保证的 , 最终的执行顺序可能是 1-2-3 也可能是 1-3-2 。如果是后者 , 则在 3 执行完毕、2 未执行之前 , 被线程二抢占了 , 这时 instance 已经是非 null 了(但却没有初始化) , 所以线程二会直接返回 instance , 然后使用 , 然后顺理成章地报错 。
我们只需要将 instance 变量声明成 volatile 就可以了 。12345678910111213141516public class Singleton {private volatile static Singleton instance; //声明成 volatileprivate Singleton (){}public static Singleton getSingleton () {if (instance == null ) {synchronized (Singleton.class) {if (instance == null ) {instance = new Singleton();}}}return instance;}}有些人认为使用 volatile 的原因是可见性 , 也就是可以保证线程在本地不会存有 instance 的副本 , 每次都是去主内存中读取 。
但其实是不对的 。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化 。
也就是说 , 在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上) , 读操作不会被重排序到内存屏障之前 。比如上面的例子 , 取操作必须在执行完 1-2-3 之后或者 1-3-2 之后 , 不存在执行到 1-3 然后取到值的情况 。