浅析 Java 中 volatile 的核心原理

 2022-09-21
原文地址:https://blog.csdn.net/superjava_/article/details/122482592

1、简介

volatile是轻量级的synchronized, 不会引起线程上下文的切换和调度 ,因此使用和执行成本更低。

2、Volatile特性

2.1、Volatile保证可见性

  • 当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值
  • 对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入

2.1.1、可见性举例

下面用一个例子来说明:

线程t1中进行循环,直到tag变成true时才退出,线程t2负责将tag更新为true,先开始t1,后开始t2

    public class VolatileTest {
    ​
        boolean tag = false;
    ​
        @Test
        public void visibleTest() throws InterruptedException {
    ​
            Thread t1 = new Thread(new Runnable() {
                @Override
                 public void run() {
                    //当tag标志变为true时结束循环并打印退出信息
                    while(!tag){
    ​
                    }
                    System.out.println("tag is true,exit......");
                }
            });
    ​
            Thread t2 = new Thread(new Runnable() {
                @Override
                 public void run() {
                    tag = true;
                }
            });
    ​
            t1.start();
            Thread.sleep(100);
            t2.start();
    ​
        }
    ​
    }
    复制代码

测试结果:

202209212225252651.png

并未打印退出信息,说明在线程t2更新tag之后,线程t1看到的tag依旧始终为false,即此时 tag在线程t2的变化对线程t1是不可见的。

此时给tag加上volatile,其它代码不变

    volatile boolean tag = false;
    复制代码

再次测试,结果:

202209212225271202.png

打印了退出信息,说明线程t1看到了tag的变化,即volatile保证了这个字段的可见性。 如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的

2.1.2、Volatile如何保证可见性?

有volatile变量修饰的共享变量进行写操作的时候,转变成汇编代码会多出一个Lock前缀的指令,这个指令有两个作用:

  • 将当前处理器缓存行的数据写回到系统内存
  • 这个写回内存的操作会使其它在CPU里缓存了该内存地址的数据无效

所以,如果对声明了volatile的变量进行写操作,这个变量所在缓存行的数据会被写回到系统内存。但此时其它处理器缓存的值还是旧的,为了保证各个处理器缓存是一致的,会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,之后对这个数据进行操作时会重新从内存中把数据读到缓存里。

2.2、Volatile不保证原子性

  • 对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。volatile变量的运算在并发情况下不是安全的。

3、Volatile的内存语义

3.1、内存语义

  • volatile写和锁的释放有相同的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存
  • volatile读和锁的获取有相同的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,从主存中读取共享变量。

3.2、volatile内存语义的实现

JMM通过限制编译器重排序和处理器重排序来实现volatile内存语义:

  • volatile写之前的操作不会被重排序到volatile写之后
  • volatile读之后的操作不会被重排序到volatile读之前
  • volatile读不会被重排序到volatile写之前

在生成字节码时插入内存屏障:

  • 在volatile写之前插入 StoreStore 屏障
  • 在volatile写之后插入 StoreLoad 屏障
  • 在volatile读之后插入 LoadLoad 屏障和LoadStore 屏障

4、Volatile使用条件

  • 运算结果不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
  • 变量不需要与其它的状态变量共同参与不变约束