【JUC】Volatile

Volatile

文章目录

  • Volatile
    • 1. 概述
    • 2. 内存屏障
    • 3. volatile可见性案例
    • 4. volatile重排序问题案例
    • 5. volatile变量的读写过程
    • 6. 使用场景

1. 概述

特点:

  • 可见性
  • 有序性-有时禁止指令重排(使用内存屏障禁止重排)

内存含义:volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中
  • 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量

2. 内存屏障

内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指今时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性

内存屏障之前的所有与操作都要回写到主内存

内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)

  • 写屏障 (Store Memory Barrier) : 在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中
  • 读屏障(Load Memory Barrier): 在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存失效,重新回到主内存中获取最新数据

四大指令

  • 在每个volatile写操作前面插入一个StoreStore屏障,强制先Store1再Store2
  • 在每个volatile写操作后面插入一个StoreLoad屏障,强制先Store1再Load2
  • 在每个volatile读操作前面插入一个LoadLoad屏障,强制先Load1再Load2
  • 在每个volatile读操作前面插入一个LoadStore屏障,强制先Load1再Store2

3. volatile可见性案例

private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
        System.out.println("线程1开始执行");
        while (flag) {}
        System.out.println("线程1结束执行");
    }).start();
    TimeUnit.SECONDS.sleep(1);
    flag = false;
    System.out.println("主线程修改flag:" + flag);
}

执行结果,且程序永不终止:

线程1开始执行
主线程修改flag:false

用volatile修饰flag变量后执行结果:

线程1开始执行
线程1结束执行
主线程修改flag:false

Process finished with exit code 0

4. volatile重排序问题案例

public class DoubleCheckSingleton {

    private static DoubleCheckSingleton instance;

    public static DoubleCheckSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckSingleton(); // 问题代码处
                }
            }
        }
        return instance;
    }
}

在问题代码处会执行如下操作;

memory = allocate(); // 	1. 分配对象的内存空间
ctorInstance(memory); // 	2. 初始化对象
instance = memory;	// 		3. 设置instance指向刚分配的内存地址

隐患:如果不用volatile修饰instance变量,由于指令重排序,2和3步骤有可能会交换执行顺序,多线程环境下其他线程得到的可能就是null,而不是完成初始化的对象

5. volatile变量的读写过程

Java内存模型中定义的8种每个线程自己的工作内存与主物理内存之间的原子操作

  • read(读取)->load(加载)->use(使用)->assign(赋值)->store(存储)->write(写入)->lock(锁定)->unlock(解锁)

  • read: 作用于主内存,将变量的值从主内存传输到工作内存

  • load: 作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载

  • use: 作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作

  • assign: 作用于工作内存,将从执行引警接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作

  • store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存

  • write: 作用于主内存,将store传输过来的变量值赋值给主内存中的变量

由于上述6条只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令

  • lock: 作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程
  • unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用

6. 使用场景

  • 单一复制可以,但是不能含复合运算赋值
  • 状态标志,判断业务是否结束
  • 读的次数远大于写,对读的变量加volatile而不需另外写锁
  • 双检锁

你可能感兴趣的:(#,03,JUC,java)