volatile在多线程中的处理

94 阅读2分钟

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关键字会导致什么问题?

  1. 因不可见性引发的问题 当前有多个线程在访问这个单例对象时,如果当前有一个线程已经进入创建对象的方法中,此时,两次判断对象是否为空的结果都是空,并且已经获取到对象锁,此时创建对象完毕,但还未刷回内存当中,此时另一个线程再来调用则仍会判断为空,重新创建对象,破坏单例。

  2. 指令重排引发问题

NSTANCE = new ConfigurationManager() 在JVM层面实际上是分为三个步骤进行执行:
1. 分配内存空间。
2. 对象初始化。
3. 将内存空间的地址赋值给对应的引用。 但是代码在JVM中执行的时候JVM会对指令优化,进行指令重排,初始化对象的过程可能会稍慢,导致cpu有所等待,因此会对2、3的位置会调换。
1. 分配内存空间
2. 将内存空间的地址赋值给对应的引用
3. 初始化对象。
此时会先开辟一块内存,然后直接将变量引用指向这块内存,这时候其他线程来调用getInstance()方法时,就会发现INSTANCE != null 然后直接把单例对象返回进行使用,可是这时候单例对象还没有进行初始化,这就导致在使用的过程中获取的对象变量可能是原有的默认值。

volatile具有什么功能?

volatile可以解决变量在多线程场景的不可见性,保证了数据的可见性,以及防止JVM进行指令重排,但不能保证数据的原子性,多线程情况下还是会出现线程安全的问题。