4.Volatile的应用

单例模式

单线程下的单例模式代码(懒汉,适用于单线程)

public class SingletonDemo {
    //用静态变量保存这个唯一实例
    private static SingletonDemo instance = null;
    
    //构造器私有化
    private SingletonDemo() {
    }
    
    //提供一个静态方法来获取实例对象
    public static SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }
}

单线程下创建出来的都是同一个对象。但是在多线程的环境下,我们的单例模式是否还是同一个对象了?

public class SingletonDemo {
    //用静态变量保存这个唯一实例
    private static SingletonDemo instance = null;

    //构造器私有化
    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 我是构造方法");
    }

    //提供一个静态方法来获取实例对象
    public static SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }

    //测试多线程是否是同一个实例!
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, "线程" + i).start();
        }
    }
}
  • 从下面的结果我们可以看出,我们通过SingletonDemo.getInstance() 获取到的对象,并不是同一个,而是被下面几个线程都进行了创建,那么在多线程环境下,单例模式如何保证呢?

解决方法1

引入synchronized关键字

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

但是synchronizaed属于重量级的同步机制,它只允许一个线程同时访问获取实例的方法,但是因此减低了并发性,因此采用的比较少。

解决方法2

通过引入DCL Double Check Lock 双端检锁机制

就是在进来和出去的时候,进行检测

public static SingletonDemo getInstance() {
    if (instance == null) {
        synchronized (SingletonDemo.class) {
            if (instance == null) {
                instance = new SingletonDemo();
            }
        }
    }
    return instance;
}

DCL确实能够保证单例模式的正确性,但是上面的方法还是存在问题的。

DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入volatile可以禁止指令重排

private static volatile SingletonDemo instance = null;

因为instance可以分为三部分进行完成:

  • memory = allocate() // 1、分配对象内存空间
  • instance(memory) // 2、初始化对象
  • instance = memory // 3、设置instance指向刚刚分配的内存地址,此时instance != null

正常来说执行完2语句再执行3语句,对象初始化后,我们才指向内存地址。

但是执行完1语句后,步骤2 和 步骤3之间不存在 数据依赖关系,而且无论重排前 还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。

这样就会造成什么问题呢?

我们先执行步骤2时,因为对象的初始化还没有完成,所以试图获取instance时,会得到null。因此执行单例模式的代码时候,就会重新再创建一个instance实例。

你可能感兴趣的:(java后端juc)