DCL double check lock
单例模式中主要有两种方式创建,一个是懒汉式,一个是饿汉式,针对懒汉式处理时会出现一种线程安全的问题。
懒汉式的创建方式:
public class ConfigurationManager {
/**
* 持久单例对象
*/
private static volatile ConfigurationManager INSTANCE = null;
/**
* 配置信息
*/
private Map<String, Object> config = null;
/**
* 获取单例对象
* @return
*/
public static ConfigurationManager getInstance() {
// 步骤一 : first check
if(null == INSTANCE) {
// 步骤二 : lock
synchronized (ConfigurationManager.class) {
// 步骤三 : second check
if(null == INSTANCE) {
// 步骤四 : 初始化单例对象
INSTANCE = new ConfigurationManager();
}
}
}
return INSTANCE;
}
/**
* 私有构造器
*/
private ConfigurationManager(){
initConfigurationFromDB();
}
/**
* 模拟初始化加载数据
*/
private void initConfigurationFromDB() {
config = new HashMap<>();
config.put("url", "www.baidu.com");
}
}
如果不加volatile关键字会导致什么问题?
-
因不可见性引发的问题 当前有多个线程在访问这个单例对象时,如果当前有一个线程已经进入创建对象的方法中,此时,两次判断对象是否为空的结果都是空,并且已经获取到对象锁,此时创建对象完毕,但还未刷回内存当中,此时另一个线程再来调用则仍会判断为空,重新创建对象,破坏单例。
-
指令重排引发问题
NSTANCE = new ConfigurationManager() 在JVM层面实际上是分为三个步骤进行执行:
1. 分配内存空间。
2. 对象初始化。
3. 将内存空间的地址赋值给对应的引用。
但是代码在JVM中执行的时候JVM会对指令优化,进行指令重排,初始化对象的过程可能会稍慢,导致cpu有所等待,因此会对2、3的位置会调换。
1. 分配内存空间
2. 将内存空间的地址赋值给对应的引用
3. 初始化对象。
此时会先开辟一块内存,然后直接将变量引用指向这块内存,这时候其他线程来调用getInstance()方法时,就会发现INSTANCE != null 然后直接把单例对象返回进行使用,可是这时候单例对象还没有进行初始化,这就导致在使用的过程中获取的对象变量可能是原有的默认值。
volatile具有什么功能?
volatile可以解决变量在多线程场景的不可见性,保证了数据的可见性,以及防止JVM进行指令重排,但不能保证数据的原子性,多线程情况下还是会出现线程安全的问题。