volatile

volatile是 Java 中的一个关键字,当一个变量是共享变量,同时被 volatile 修饰当值被更改的时候,其他线程再读取该变量的时候可以保证能获取到修改后的值,通过 JMM 屏蔽掉各种硬件和操作系统的内存访问差异 以及 CPU 多级缓存等导致的数据不一致问题。
需要注意的是,volatile 修饰的变量对所有线程是立即可见的,关键字本身就包含了禁止指令重排的语意,但是在非原子操作的并发读写中是不安全的,比如 i++ 操作一共分三步操作。
相比 synchronized Lock,volatile 更加轻量级,不会发生上下文切换等开销,接着分析下它的适用场景,以及错误使用场景。

volatile 的作用

  • 保证可见性:Happens-before 关系中对于 volatile 是这样描述的:对一个 volatile 变量的写操作 happen-before 后面对该变量的读操作。这就代表了如果变量被 volatile 修饰,那么每次修改之后,接下来在读取这个变量的时候一定能读取到该变量最新的值。
  • 禁止指令重排:先介绍一下 as-if-serial 语义:不管怎么重排序,(单线程)程序的执行结果不会改变。在满足 as-if-serial 语义的前提下,由于编译器或 CPU 的优化,代码的实际执行顺序可能与我们编写的顺序是不同的,这在单线程的情况下是没问题的,但是一旦引入多线程,这种乱序就可能会导致严重的线程安全问题。用了 volatile 关键字就可以在一定程度上禁止这种重排序。

volatile 正确用法

boolean 标志位

共享变量只有被赋值和读取,没有其他的多个复合操作(比如先读数据再修改的复合运算 i++),我们就可以使用 volatile 代替 synchronized 或者代替原子类,因为赋值操作是原子性操作,而 volatile 同时保证了 可见性,所以是线程安全的。
如下经典场景 volatile boolean flag,一旦 flag 发生变化,所有的线程立即可见。
volatile boolean shutdownRequested;

public void shutdown() {
    shutdownRequested true;
}

public void doWork() {
    while (!shutdownRequested) {
        // do stuff
    }
}
线程 1 执行 doWork() 的过程中,可能有另外的线程 2 调用了 shutdown,线程 1 里吗读区到修改的值并停止执行。
这种类型的状态标记的一个公共特性是:通常只有一种状态转换,shutdownRequested 标志从false 转换为true,然后程序停止。

volatile 错误用法

volatile 不适合运用于需要保证原子性的场景,比如更新的时候需要依赖原来的值,而最典型的就是 a++ 的场景,我们仅靠 volatile 是不能保证 a++是原子性的。代码如下所示:
public class TestVolatile implements Runnable {
    volatile int a;
    public static void main(String[] args) throws InterruptedException {
        Runnable r =  new TestVolatile();
        Thread thread1 new Thread(r);
        Thread thread2 new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(((TestVolatile) r).a);
    }
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            a++;
        }
    }
}
最终的结果 a < 2000。
 
posted on 2023-03-29 21:58  zhengbiyu  阅读(26)  评论(0编辑  收藏  举报