zl程序教程

您现在的位置是:首页 >  大数据

当前栏目

并发编程 - 单例模式讲解

并发模式编程 讲解 单例
2023-09-11 14:16:28 时间

单例模式的两种创建形式

积极加载,在类加载的时候直接创建初始化对象,代码如下:

    /**
     * 如果是饿汗模式 直接初始化 每个对象获取都是一个对象
     * <p>
     * 懒加载
     */
    public static SingleInstance instance = new SingleInstance();

    //第一种
    public static SingleInstance getInstance1() {
        if (instance == null) {
            instance = new SingleInstance();
        }
        return instance;

    }

对于大量没有使用的对象,因为频繁创建对象会消耗额外的内容,所以一般会根据具体场景使用另外一种加载模式,当然上面这种模式的优点在于书写简单,且线程安全

懒汉式加载的几种实现

第一种,简单明了

 public static SingleInstance instance2;

    /**
     *  多线程模式下 如果线程1走到了3  线程2走到了1 或者2 都会生成新的instance
     * 这个 需要加锁解决 避免多次创建对象 消耗资源 且造成线程之间共享数据不安全
     *
     * @return
     */
    public static SingleInstance getInstance2() {
        if (instance == null) { //1
            instance = new SingleInstance();  //2
        }
        return instance; //3

    }

针对第一种现象,为了保证线程之间的安全性,我们采用加锁的方式

    public static synchronized SingleInstance getInstance3() {
        if (instance == null) {
            instance = new SingleInstance();
        }
        return instance;

    }

加锁 虽然给方法加锁可以避免前面造成的问题。但是如果频繁地访问这个对象, 那么这种频繁加锁和释放锁方式就会产生严重的效率问题。为了保证并发效率,采用双重检测

public static synchronized SingleInstance getInstance4() {
        if (instance == null) { //1
            synchronized (SingleInstance.class){//2
                //加类锁
                if(instance ==null){ //3
                    instance = new SingleInstance(); //4
                }
            }
        }
        return instance; //5
    }

双重检测

虽然加了锁和双重判断,但其实这个类是线程不安全的。
现象是线程A进入同步块创建实例的时候,线程B会返回一个没有初始化的Instance对象。
原因是JIT编译器会优化,会在编译时会发生“重排序”的状况。

 线程A 走到了 5 线程B 走到了2 这个时候因为线程A已经释放了类锁 所以线程B就会进入到 3 再次判断 instance是否为空,
 这就是JVM 模型的问题 , 因为每一个线程的工作内存都拷贝一份

指令重排序
编译器遇到singleton = new xxxxx();会分为如下三个步骤:

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

Java语言规范没有规定编译器的优化不会改变单线程的执行结果,但是并没有对多线程做出这样的保证。多线程情况下有时候编译器会对指令进行重排序优化,它可能把2和3颠倒过来,如下:

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

结合指令重排序分析上面的双重检测机制,就会发现 当线程A走到5的时候,先执行了instance = memory; 然后线程B执行到1时,发现singleton不为null,那么就获得了singleton对象。然而这个singleton对象其实还没有经过ctorInstance(memory),这是不正确的。

所以为了保证双重检测的安全性,禁止指令重排序,在实际开发中会采用 volatile关键字,volatile的具体使用,以及volatile是如何利于JVM内容屏障机制以及JMM来保证可见性以及禁止指令重排

public static volatile SingleInstance instance = new SingleInstance();

public static synchronized SingleInstance getInstance4() {
     if (instance == null) { //1
         synchronized (SingleInstance.class){//2
             //加类锁
             if(instance ==null){ //3
                 instance = new SingleInstance(); //4
             }
         }
     }
     return instance; //5
 }