volatile原理(内存屏障)

volatile

场景
  • 一个线程写,其他线程读的情况

  • double-check-lock时,synchronized同步代码块外共享变量的指令重排序问题

同步机制

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

  • 保证可见性

  • 不保证原子性

  • 保证有序性(禁止指令重排)

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

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

  • 加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞,所以同一时间只有一个线程执行,相当于单线程,由于数据依赖性的存在,单线程的指令重排是没有问题的

  • 线程加锁前,将清空工作内存中共享变量的值,使用共享变量时需要从主内存中重新读取最新的值;线程解锁前,必须把共享变量的最新值刷新到主内存中(JMM 内存交互章节有讲)


指令重排

volatile 修饰的变量,可以禁用指令重排( JIT实现 )

使用volatile,加在最后赋值的变量上,可以防止她之前的代码指令重排序【内存屏障】

指令重排实例:

  • example 1:

     public void mySort() {
         int x = 11; //语句1
         int y = 12; //语句2  谁先执行效果一样
         x = x + 5;  //语句3
         y = x * x;  //语句4
     }

    执行顺序是:1 2 3 4、2 1 3 4、1 3 2 4

    指令重排也有限制不会出现:4321,语句 4 需要依赖于 y 以及 x 的申明,因为存在数据依赖,无法首先执行

  • example 2:

     int num = 0;
     boolean ready = false;
     // 线程1 执行此方法
     public void actor1(I_Result r) {
         if(ready) {
             r.r1 = num + num;
         } else {
             r.r1 = 1;
         }
     }
     // 线程2 执行此方法
     public void actor2(I_Result r) {
         num = 2;
         ready = true;
     }

    情况一:线程 1 先执行,ready = false,结果为 r.r1 = 1

    情况二:线程 2 先执行 num = 2,但还没执行 ready = true,线程 1 执行,结果为 r.r1 = 1

    情况三:线程 2 先执行 ready = true,线程 1 执行,进入 if 分支结果为 r.r1 = 4

    情况四:线程 2 执行 ready = true,切换到线程 1,进入 if 分支为 r.r1 = 0,再切回线程 2 执行 num = 2,发生指令重排


底层原理
缓存一致

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

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

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

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

内存屏障有三个作用:

  • 确保对内存的读-改-写操作原子执行

  • 阻止屏障两侧的指令重排序

  • 强制把缓存中的脏数据写回主内存,让缓存行中相应的数据失效


内存屏障

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

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

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

根据内存屏障指令解释: “写volatile类型变量时” 写操作之前, 底层会插入一个"StoreStore"屏障指令,后续的写之前要保证第一个sotre写已经执行完毕并刷出到主内存 写操作之后, 底层会插入一个"StoreLoad"屏障指令,保证写操作已经刷新到主内存后,才允许后续的load读操作执行,并防止与后面的volatile读产生重排序

根据内存屏障指令解释: “读volatile类型变量时” 读操作之前, 会插入一个"LoadLoad"指令,保证后续的读操作,要在前面的读操作执行完毕后执行,并禁止前面的volatile读与普通类型数据的读产生重排序 读操作之后,会插入一个"LoadStore"指令,保证在读之前,前面的写操作要执行完毕,并将数据刷新到主内存后执行,并禁止读之前当前数据前面的votalie读与当前读之前后面的普通读产生重排序

保证可见性

  • 写屏障(sfence,Store Barrier)保证在该屏障之前的,对共享变量的改动,都同步到主存当中【 就不会写到工作内存当中了,顺带把以前的也同步到主内存当中

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

     public void actor1(I_Result r) {
         // 读屏障
         // ready 是 volatile 读取值带读屏障
         if(ready) {
             r.r1 = num + num;
         } else {
             r.r1 = 1;
         }
     }

  • 全能屏障:mfence(modify/mix Barrier),兼具 sfence 和 lfence 的功能

保证有序性:(屏障之后的操作仍然需要遵循数据依赖性和内存顺序模型的规则,并且不会被随意地重排序到屏障之前???)

  • 写屏障会确保指令重排序时,将写屏障之前的代码不会排在写屏障之后。【 之前的不会重排序到后面,跑 / 排 指的是重排序 】才不会被旧数据覆盖

  • 读屏障会确保指令重排序时,将读屏障之后的代码不会排在读屏障之前。【 之后的不会排到前面去 】才不会读到未来的数据

不能解决指令交错:

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

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

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

    i++ 反编译后的指令:

     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 操作,防止指令重排


双端检锁
检锁机制

Double-Checked Locking单例模式:双端检锁机制。【 实现只有第一次会锁竞争 】

【类似Balking模式】

DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入 volatile 可以禁止指令重排

 public final class Singleton {
     private Singleton() { }
     private static Singleton INSTANCE = null;
     
     public static Singleton getInstance() {
         if(INSTANCE == null) { // t2,这里的判断不是线程安全的。      因为INSTANCE并没有完全被synchronized完全保护起来
             // 首次访问会同步,而之后的使用没有 synchronized,只有第一次会有竞争
             synchronized(Singleton.class) {
                 // 这里是线程安全的判断,防止其他线程在当前线程等待锁的期间完成了初始化
                 if (INSTANCE == null) { 
                     INSTANCE = new Singleton();
                 }
             }
         }
         return INSTANCE;
     }
 }
 ​
 synchronized(class){
     if(x == null){
         这样子的话每次都会竞争,还是锁类对象,耗能
     }
 }

不锁 INSTANCE 的原因:

  • INSTANCE 要重新赋值

  • INSTANCE 是 null,线程加锁之前需要获取对象的引用,设置对象头,null 没有引用

实现特点:

  • 懒惰初始化

  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁

  • 第一个 if 使用了 INSTANCE 变量,是在同步块之外(此时synchronized的三个特性保证不了不指令重排序),但在多线程环境下会产生问题


DCL问题

getInstance 方法对应的字节码为:

 0:  getstatic       #2      // Field INSTANCE:Ltest/Singleton;
 3:  ifnonnull       37
 6:  ldc             #3      // class test/Singleton
 8:  dup                     //类对象指针存储一份方便后面解锁,存在栈帧?Monitor?
 9:  astore_0
 10: monitorenter
 11: getstatic       #2      // Field INSTANCE:Ltest/Singleton;
 14: ifnonnull 27
 17: new             #3      // class test/Singleton
 20: dup
 21: invokespecial   #4      // Method "<init>":()V      【 INSTANCE = new Singleton();】
 24: putstatic       #2      // Field INSTANCE:Ltest/Singleton;
 27: aload_0
 28: monitorexit
 29: goto 37
 32: astore_1
 33: aload_0
 34: monitorexit
 35: aload_1
 36: athrow
 37: getstatic       #2      // Field INSTANCE:Ltest/Singleton;
 40: areturn
  • 17 表示创建对象,将对象引用入栈

  • 20 表示复制一份对象引用,引用地址

  • 21 表示利用一个对象引用,调用构造方法初始化对象 【synchronized内部不是原子的还是会指令重排序,有序性是保证之前的不排到同步代码块后面,volatile才能阻止重排序】------【内部重排序是因为单线程内重排序不会对结果造成影响(但被其他线程插空子了)所以可以重排。】

  • 24 表示利用一个对象引用,赋值给 static INSTANCE

步骤 21 和 24 之间不存在数据依赖关系,而且无论重排前后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许

  • 关键在于 0:getstatic 这行代码在 monitor 控制之外,可以越过 monitor 读取 INSTANCE 变量的值

  • 当其他线程访问 INSTANCE 不为 null 时,由于 INSTANCE 实例未必已初始化,那么 t2 拿到的是将是一个未初始化完毕的单例返回,这就造成了线程安全的问题


解决方法

指令重排只会保证串行语义的执行一致性(单线程),但并不会关心多线程间的语义一致性

引入 volatile,来保证不出现指令重排的问题,从而保证单例模式的线程安全性:

 private static volatile SingletonDemo INSTANCE = null;

【volatile通过屏障保障读屏障(后面的不会跑到前面)的时候,已经完成了构造方法(写屏障赋值之前的代码不会跑到后面也就是执行完构造方法了),即写屏障在读屏障之前】


happens-before规则

[ 访问共享变量时,你的写入是否对其他线程是可见的 ]

happens-before 先行发生

Java 内存模型具备一些先天的“有序性”,即不需要通过任何同步手段(volatile、synchronized 等)就能够得到保证的安全,这个通常也称为 happens-before 原则,它是可见性与有序性的一套规则总结

不符合 happens-before 规则,JMM 并不能保证一个线程的可见性和有序性

  1. 程序次序规则 (Program Order Rule):一个线程内,逻辑上书写在前面的操作先行发生于书写在后面的操作 ,因为多个操作之间有先后依赖关系,则不允许对这些操作进行重排序

  2. 锁定规则 (Monitor Lock Rule):一个 unlock 操作先行发生于后面(时间的先后)对同一个锁的 lock 操作,所以线程解锁 m 之前对变量的写(解锁前会刷新到主内存中),对于接下来对 m 加锁的其它线程对该变量的读可见

  3. volatile 变量规则 (Volatile Variable Rule):对 volatile 变量的写操作先行发生于后面对这个变量的读

  4. 传递规则 (Transitivity):具有传递性,如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C

  5. 线程启动规则 (Thread Start Rule):Thread 对象的 start()方 法先行发生于此线程中的每一个操作

     static int x = 10;//线程 start 前对变量的写,对该线程开始后对该变量的读可见
     new Thread(()->{    System.out.println(x);  },"t1").start();
  6. 线程中断规则 (Thread Interruption Rule):对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生

  7. 线程终止规则 (Thread Termination Rule):线程中所有的操作都先行发生于线程的终止检测,可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行

  8. 对象终结规则(Finaizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始


设计模式

终止模式

终止模式之两阶段终止模式:停止标记用 volatile 是为了保证该变量在多个线程之间的可见性【之前使用interrupt实现】

 class TwoPhaseTermination {
     // 监控线程
     private Thread monitor;
     // 停止标记
     private volatile boolean stop = false;;
 ​
     // 启动监控线程
     public void start() {
         monitor = new Thread(() -> {
             while (true) {
                 Thread thread = Thread.currentThread();
                 if (stop) {
                     System.out.println("后置处理");
                     break;
                 }
                 try {
                     Thread.sleep(1000);// 睡眠
                     System.out.println(thread.getName() + "执行监控记录");
                 } catch (InterruptedException e) {
                     System.out.println("被打断,退出睡眠");
                 }
             }
         });
         monitor.start();
     }
 ​
     // 停止监控线程
     public void stop() {
         stop = true;
         monitor.interrupt();// 让线程尽快退出Timed Waiting======尽管打断sleep会抛异常
     }
 }
 // 测试
 public static void main(String[] args) throws InterruptedException {
     TwoPhaseTermination tpt = new TwoPhaseTermination();
     tpt.start();
     Thread.sleep(3500);
     System.out.println("停止监控");
     tpt.stop();
 }


Balking

[也是监控业务场景相关的]

Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回

 public class MonitorService {
     // 用来表示是否已经有线程已经在执行启动了
     private volatile boolean starting = false;  ///用在停止监控线程时,将starting置为false;要让Tomcat多线程知道他的变化!
     public void start() {
         System.out.println("尝试启动监控线程...");
         synchronized (this) {
             if (starting) {
                 return;
             }
             starting = true;
         }
         // 真正启动监控线程...
     }
     
     
 }

对比保护性暂停模式:保护性暂停模式用在一个线程等待另一个线程的执行结果,当条件不满足时线程等待

例子:希望 doInit() 方法仅被调用一次,下面的实现出现的问题:

  • 当 t1 线程进入 init() 准备 doInit(),t2 线程进来,initialized 还为f alse,则 t2 就又初始化一次

  • volatile 适合一个线程写,其他线程读的情况,这个代码需要加锁

 public class TestVolatile {
     volatile boolean initialized = false;
     
     void init() {
         if (initialized) {
             return;
         }
         doInit();
         initialized = true;
     }
     private void doInit() {
     }
 }

还经常用在线程安全的单例模式

 public final class singleton{   ///final保证不被重写,破坏单例的方法
     private singleton(){
     }
     private static singleton INSTANCE = null;
     
     public static synchronized singleton getInstance(){     //synchronized保证
          if(INSTANCE != nu11){
               return INSTANCE;
          }
     
         INSTANCE = new singleton();
         return INSTANCE;
     }
 }

对比保护性暂停模式:保护性暂停模式用在一个线程等待另一个线程的执行结果,当条件不满足时线程等待


  • 29
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Volatile是一种Java中的关键字,用于标识变量是易变的,即该变量的值可能会在不同的线程中发生改变。Volatile底层原理涉及到Java内存模型。 Java内存模型定义了线程如何与内存交互以及线程之间如何共享内存。Java内存模型将内存分为主内存和线程工作内存。主内存是所有线程共享的内存区域,而线程工作内存是每个线程独立拥有的内存区域。 当一个线程访问一个volatile变量时,它会从主内存中读取最新的值。而当一个线程更新一个volatile变量时,它会将新的值立即写入主内存中。这保证了所有线程对volatile变量的读写操作都是可见的。 此外,volatile还具有禁止指令重排序的作用。在多线程并发编程中,编译器为了提高程序执行效率可能会对指令顺序进行重排序,但是这种重排序可能会导致并发问题。使用volatile可以禁止编译器对volatile变量的指令进行重排序,保证了程序的正确性。 总之,volatile的底层原理是基于Java内存模型的,它保证了多线程环境下对volatile变量的可见性和禁止指令重排序的特性。 ### 回答2: VolatileJava中的关键字之一,用于修饰变量,主要用于多线程编程中,以保证线程间变量的可见性和顺序性。 Volatile的底层原理主要是通过内存屏障(Memory Barrier)和禁止重排序来实现的。内存屏障是一种CPU指令,能够强制刷新处理器缓存并保证读/写操作顺序的一致性。当一个线程修改了一个被volatile修饰的变量的值时,会立即将该值刷新到主内存,并通知其他线程对对应变量的缓存失效,强制其他线程从主内存重新读取最新值。 此外,volatile还可以禁止指令重排,保证代码的有序执行。在有volatile修饰的变量之前的指令一定会在其后的指令之前执行。这样可以避免了由于指令重排导致的数据不一致问题。 总之,Volatile底层原理主要通过内存屏障以及禁止指令重排来保证线程间变量的可见性和顺序性。它能够确保一个变量在多个线程之间的可见性,尤其用于一个线程修改了变量值时,其他线程能够立即感知到变量的变化,并从主内存中重新读取最新值,从而避免了线程间数据不一致的问题。同时,它还通过禁止指令重排,保证了代码的有序执行,避免了由于指令重排导致的逻辑错误。因此,在多线程编程中,合理使用Volatile关键字能够确保程序的正确性和稳定性。 ### 回答3: VolatileJava中的关键字,用于修饰变量。它的底层原理是通过禁止线程内部的缓存变量副本,直接访问主存中的变量值,保证了多线程环境中的可见性和有序性。下面详细解释其底层原理。 在多线程环境下,每个线程都有自己的工作内存(线程的私有内存),存放变量的副本。由于性能原因,线程在执行操作时,通常会先将变量从主存中读取到工作内存中进行操作,然后再将修改的结果写回主存。这种操作称为“读写操作的优化”。 当一个变量被volatile修饰时,它的读写操作会具有特殊的语义。当一个线程对volatile修饰的变量进行写操作时,它会首先将值写入工作内存,然后立即刷新到主存中,并且通知其他线程该变量的值已经被修改。而当一个线程对volatile变量进行读操作时,它会立即从主存中读取最新的值,并且在读之前使自己的工作内存失效,以保证读操作获取的是最新值。 这种特殊的语义使得volatile能够保证多线程环境下的可见性和有序性。通过禁止线程内部的缓存变量副本,保证了每个线程对volatile变量的读写操作都是基于主存中最新的值,从而避免了数据不一致的问题。同时,由于读操作会使工作内存失效,写操作会立即刷新到主存,保证了变量的修改对其他线程的可见性和顺序性。 总结起来,volatile的底层原理是通过禁止线程内部的变量副本,直接访问主存中的变量值,保证了在多线程环境下的可见性和有序性。它对于一些简单的变量操作可以替代锁,同时也可以用于线程间的通信,但并不能保证原子性。因此,在使用volatile时,需要根据具体的场景和需求来判断是否合适。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值