这个volatile用对了吗?

298 阅读3分钟

一、背景

在项目中有这么一段代码,其中configs 这个变量使用了static volatile 两个关键字;对于这个变量是否需要加 volatile 关键字,有点疑惑。其中 load 方法是在项目启动和new 线程中读取到新的配置会调用;而isHitGray 方法在大量的业务代码中被调用。

public static volatile String con'fi'g's;
public static synchronized void load(Map<String, String> merchantConfigContent ) {
    configs=merchantConfigContent.get(KEY);
}
public static boolean isHitGray(String planId){
    if (StringUtils.isBlank(planId)) {
        return false;
    }
    return configs.contains(planId);
}

二、volatile关键字

当一个变量被定义为volatile 之后会获得两项特性:可见性和禁止指令重排。

1. 可见性

在java中,每一个线程都有一个自己的工作内存,在默认情况下,为了提高效率,会将共享变量拷贝一份到自己的工作线程当中,例如上面的configs变量,如下图。但是如果其中一个线程对共享变量进行了更新之后,会导致其他线程中的共享变量并不是最新的的,就会导致数据不一致的问题。而加了volatile之后,java工作线程更新之后,会同步其他工作线程,其他工作线程读取共享变量的时候,发现已过期会重新从主内存中读取,由此保证可见性。

image-13.png 下面的代码中展示了,如果不使用 volatile,会由于可见性导致死循环,第一个线程更新了共享变量,但是线程2并不能读取到。

@Test
public void test() throws InterruptedException {

    Thread writerThread = new Thread(() -> {
        try {
            Thread.sleep(1000); // 让读线程先运行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true; // 修改共享变量
        System.out.println("Writer thread set flag to true");
    });

    Thread readerThread = new Thread(() -> {
        while (!flag) {
            // 不断检查共享变量的值
        }
        System.out.println("Reader thread saw flag is true");
    });

    readerThread.start();
    writerThread.start();

    readerThread.join();
    writerThread.join();
}

2. 禁止指令重排

使用volatile 变量的第二个语义是禁止指令重排序优化,普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证 ”变量赋值操作“的顺序与程序代码中的执行顺序一致。因为在同一个线程的方法执行过程中无法感知到这点,这就是Java内存模型中描述的所谓“线程内表现为串行的语义”(As-If-Serial)。

例如下面这段伪代码,如果flag = true 的操作提前,会导致 buy 中的操作异常。

private static boolean flag = false;
public void update() {
    //这里做商品上架,增加库存的操作
    add();
    flag = true;
}
public void buy() {
    while (!flag) {
        //等待
        sleep();
    }
    //这里做库存扣减的操作
    del();
}

三、synchronized关键字

synchronized是jvm提供的一个同步关键字,通过synchronized关键字,我们可以实现”一个变量在同一个时刻只允许一条线程对其进行lock操作“。synchronized关键字可以使用在代码块和方法上。当synchronized关键字加在方法上的时候,此时synchronized方法会获取当前类的类锁。但是需要注意,synchronized方法并不会阻塞其他非synchronized方法。

四、结论

那么回归最开始的问题, configs 在load方法中进行写操作,在isHitGray 方法进行读操作;load方法是synchronized 方法,但是 isHitGray 方法却不是,因此当load方法在更新 configs 变量的时候,isHitGray方法并不会阻塞;因此可能会出现更新后,其他线程在调用 isHitGray 方法的时候,获取到的不是最新的变量,因此我们需要通过 volatile 关键字保证可见性。

最终我们可以得到一个结论: configs 变量上的 volatile 关键字是必要的,为了保证并发情况下的可见性。