JUC-volatile原理

volatile 是 Java 虚拟机提供的轻量级的同步机制(三大特性)

  • 保证可见性
  • 不保证原子性 (原子性由之前介绍的各种锁保证)
  • 保证有序性(禁止指令重排)

性能:volatile 修饰的变量进行读操作与普通变量几乎没什么差别,但是写操作相对慢一些,因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行,但是开销比锁要小

synchronized 无法禁止指令重排和处理器优化,为什么可以保证有序性可见性

  • 加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞,所以同一时间只有一个线程执行,相当于单线程,由于数据依赖性的存在,单线程的指令重排是没有问题的
  • 线程加锁前,将清空工作内存中共享变量的值,使用共享变量时需要从主内存中重新读取最新的值;线程解锁前,必须把共享变量的最新值刷新到主内存中(JMM 内存交互章节有讲)

底层原理

缓存一致

使用 volatile 修饰的共享变量,底层通过汇编 lock 前缀指令进行缓存锁定,在线程修改完共享变量后写回主存,其他的 CPU 核心上运行的线程通过 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据

lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

内存屏障有三个作用:

  • 确保对内存的读-改-写操作原子执行
  • 阻止屏障两侧的指令重排序
  • 强制把缓存中的脏数据写回主内存,让缓存行中相应的数据失效

内存屏障

保证可见性

  • 写屏障(sfence,Store Barrier)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

    1
    2
    3
    4
    5
    public void actor2(I_Result r) {
    num = 2;
    ready = true; // ready 是 volatile 赋值带写屏障
    // 写屏障
    }
  • 读屏障(lfence,Load Barrier)保证在该屏障之后的,对共享变量的读取,从主存刷新变量值,加载的是主存中最新数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void actor1(I_Result r) {
    // 读屏障
    // ready 是 volatile 读取值带读屏障
    if(ready) {
    r.r1 = num + num;
    } else {
    r.r1 = 1;
    }
    }
  • 全能屏障:mfence(modify/mix Barrier),兼具 sfence 和 lfence 的功能

保证有序性

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

不能解决指令交错:

  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其他线程的读跑到写屏障之前

  • 有序性的保证也只是保证了本线程内相关代码不被重排序

    1
    2
    3
    volatile i = 0;
    new Thread(() -> {i++});
    new Thread(() -> {i--});

    i++ 反编译后的指令:

    1
    2
    3
    0: iconst_1			// 当int取值 -1~5 时,JVM采用iconst指令将常量压入栈中
    1: istore_1 // 将操作数栈顶数据弹出,存入局部变量表的 slot 1
    2: iinc 1, 1

交互规则

对于 volatile 修饰的变量:

  • 线程对变量的 use 与 load、read 操作是相关联的,所以变量使用前必须先从主存加载
  • 线程对变量的 assign 与 store、write 操作是相关联的,所以变量使用后必须同步至主存
  • 线程 1 和线程 2 谁先对变量执行 read 操作,就会先进行 write 操作,防止指令重排
文章作者: GeYu
文章链接: https://nuistgy.github.io/2023/04/23/JUC-volatile原理/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Yu's Blog