原来单例模式可以这样实现

单例模式是设计模式中最简单的一个模式,也是常被忽略的一个设计模式。如果不对单例模式进行一次认真的研究,真不见得能写出让自己满意的一种实现,即考虑岛安全和效率。单例模式有两种实现方式---饿汉式和懒汉式。

饿汉式单例模式就是在程序启动时就完成了初始化,这种实现比较简单

//饿汉式
public class Singleton {
    private static Singleton instance = new Singleton();    //1,单例对象的引用
        
    private Singleton() {}  //2,声明构造函数私有
    
    public static Singleton getInstance() {     //3,获取单例对象方法
        
        return instance;                    
    }
}
饿汉式中的单例对象instance是在对类Singleton初始化中创建的,在多线程环境下也是安全的。因为jvm在对类Singleton初始化时就考虑了多线程并发带来的问题。

懒汉式单例模式是在第一次使用到单例对象来创建的,在单线程环境下下面的代码是ok的

//懒汉式
public class Singleton {
    private static Singleton instance = null;    //1,单例对象的引用

    private Singleton() {}  //2,声明构造函数私有

    public static Singleton getInstance() {     //3,获取单例对象方法
        if (instance == null) {                 //4,判断是否为null
            instance = new Singleton();         //5,创建单例对象
        }
        return instance;
    }
}

但处在多线程环境下,这种实现就变得不安全了。假如有A,B两个线程同时执行4,instance == null 的结果都为true,所以A线程,B线程都会 new Singleton(); 就会导致两次创建了单例对象,如果有更多的线程同时访问,就可能不止创建了两次单例对象。为了防止多次创建单例对象,加锁对线程进行同步,所以就有了下面的代码

//懒汉式
public class Singleton {
    private static Singleton instance = null;    //1,单例对象的引用

    private Singleton() {}  //2,声明构造函数私有

    public static synchronized Singleton getInstance() {     //3,获取单例对象方法
        if (instance == null) {                 //4,判断是否为null
            instance = new Singleton();         //5,创建单例对象
        }
        return instance;
    }
}

在getInstance方法上加上synchronized进行同步,这次不会肯定不会创建多次单例对象了。现在确实是线程安全了,但引进来了效率问题,因为同时只能有一个线程访问单例对象。只有第一个获得锁的线程才会去new Singleton(); 以后的线程只是读这个单例对象。多个线程不能同时读一个不变的单例对象显然效率还有进步的空间。就出现了著名的双重检查锁就是两次对instance == null判断,下面是双重检查锁的代码

//懒汉式---双重检查锁
public class Singleton {
    private static Singleton instance = null;    //1,单例对象的引用

    private Singleton() {}  //2,声明构造函数私有

    public static Singleton getInstance() {     //3,获取单例对象方法
        if (instance == null) {                 //4,第一次判断instance是否为null
            synchronized (Singleton.class) {    //同步块
                if (instance == null) {         //5,第二次判断instance是否为null
                    instance = new Singleton(); //6,创建单例对象
                }
            }
        }
        return instance;
    }
}

完美了吧,当instance为null时,同时访问getInstance的线程都会判断instance==null,但同时只能有一个线程进入同步块,在同步块中第二次判断instance == null, 如果为null就会去创建单例对象,当单例对象创建好以后,以后的线程再访问单例对象时,就会直接return instance,再也不会进入同步块,这样多个线程就可以并发访问单例对象了,是不是很巧妙哈哈。仔细推敲,上面的代码还是有问题的,问题就出现在 instance = new Singleton(); 因为它不是一个原子操作,不是原子操作也就算了,关键java还会重排序,就是这个原因引发的问题。先看一下instance = new Singleton(); 可以拆成的3行伪代码

memory = allocate(); //1,分配对象的内存空间
ctorInstance(memory); //2,初始化对象
instance = memory; //3,设置instance指向刚才分配的内存空间

如果按上面的执行顺序也是没有问题的,由于存在指令重排序,cpu实际执行的顺序可能是1, 3, 2。这样潜在的问题就出现了,在还没有对memory进行初始化之前,已经将instance 指向了memory,此时若有一个线程刚执行4,第一次判断instance是否为null,那么线程就会返回一个未完成初始化的instance。恶心的是2, 3可能会重排序,那就禁止这种重排序呗,看下面的代码
//懒汉式---双重检查锁
public class Singleton {
    private static volatile Singleton instance = null;    //1,单例对象的引用

    private Singleton() {}  //2,声明构造函数私有

    public static Singleton getInstance() {     //3,获取单例对象方法
        if (instance == null) {                 //4,判断是否为null
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

对没有看错,就是把instance生命为volatile变量即可。java内存模型会禁止对volatile变量的写与它之前的任何指令进行重排序。这样一个完美的单例模式就被实现出来了。除了双重检查锁,还有一种实现方式就是使用嵌套类,就是通过利用jvm对java类的初始化的同步来完成的,代码如下

public class InstanceHoder {
    public static class Singleton {
        private static Singleton singleton = new Singleton();
        public static Singleton getInstance() {
            return singleton;
        }
    }
}



你可能感兴趣的:(java)