volatile 关键字 (详细解析)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: volatile 关键字 的工作原理:1、子线程t 和 main方法从主内存读取到数据放入其对应的工作内存,此时 flag的值为false2、子线程t 将flag的值更改为true3、在某一时刻 子线程t将flag的值写回主内存后,失效其他线程对此变量副本4、main方法 再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中

前置知识

共享变量不可见性

       在多线程并发执行下,多个线程修改共享的成员变量,会出现一个线程修改了共享变量的值后,另一个线程不能直接看到该线程修改后的变量的最新值


代码实例

publicclassVisibilityDemo01 {
// main方法,作为一个主线程。publicstaticvoidmain(String[] args) {
// a.开启一个子线程MyThreadt=newMyThread();
t.start();
// b.主线程执行while(true){
if(t.isFlag()){
System.out.println("主线程进入循环执行~~~~~");
            }
        }
    }
}
classMyThreadextendsThread{
// 成员变量privatebooleanflag=false;
@Overridepublicvoidrun() {
try {
Thread.sleep(1000);
        } catch (InterruptedExceptione) {
e.printStackTrace();
        }
// 触发修改共享成员变量flag=true;
System.out.println("flag="+flag);
    }
publicbooleanisFlag() {
returnflag;
    }
publicvoidsetFlag(booleanflag) {
this.flag=flag;
    }
}

image.gif

image.gif


我们看到,子线程中已经将flag设置为true,但main()方法中始终没有读到修改后的最新值,从而循环没有能进入到if语句中执行,所以没有任何打印 , 这就是变量的不可见性

JMM

注意区别JMM和JVM

JVM和JMM是有区别的,它们是两个不同的概念:


  • JVM是Java Virtual Machine(Java虚拟机)的缩写,它是Java编程语言的核心组件之一。JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM负责执行Java程序的指令,并提供一些高级功能,如垃圾回收、内存管理、线程调度等。
  • JMM是Java Memory Model(Java内存模型)的缩写,它是Java虚拟机规范中定义的一种抽象的概念。JMM定义了线程和主内存之间的抽象关系,即JMM中定义了线程在JVM主内存中的工作方式。JMM规范了Java虚拟机与计算机内存是如何协同工作的,包括如何读取和写入共享变量,以及在必要时如何同步访问共享变量。


JVM负责执行Java程序,并提供高级功能,而JMM则定义了线程和内存之间的抽象关系,以确保Java程序在多线程环境下的正确性


工作内存 和 主内存概念

image.gif


JMM规定如下:

    • 所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
    • 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
    • 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
    • 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。


    现在就可以解释 共享变量不可见性 的原因

      1. 子线程t 和 main方法 从主内存读取到数据放入其对应的工作内存(子线程t 和 main方法 谁先执行不一定),此时 flag 的值为 false
      2. 子线程t 睡眠1秒后,将flag的值更改为true,但是这个时候flag的值还没有写回主内存
      3. 当 子线程t flag的值写回去后,但是main方法不会再去读取主存中的值,而是读取自己工作内存中的 flag变量副本,所以while(true)读取到的值一直是false(虽然 main方法 可能会在某一时刻读取主内存中flag 的最新值来刷新flag变量副本,但这个时间我们是无法控制的)


      为什么 main方法要去 读取自己工作内存中的 flag变量副本,而不每次都去主内存中读取,这类似 多级缓存的概念,线程从自己的工作内存中读取数据的速度会快于从主内存中读取数据的速度


      volatile 关键字

      如何实现在多线程下访问共享变量的可见性:也就是实现一个线程修改变量后,对其他线程可见呢?有两种方法

      第一种是使用volatile关键字

      第二种是加锁


      使用volatile关键字

      使用volatile关键字修改该变量

      privatevolatilebooleanflag ;


      运行结果

      image.gif


      我们看到  使用volatile关键字解决了 共享变量不可见性的问题,即 一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。


      工作原理

        1. 子线程t 和 main方法 从主内存读取到数据放入其对应的工作内存,此时 flag的值为false
        2. 子线程t 将flag的值更改为true
        3. 在某一时刻 子线程t flag的值写回主内存后,失效其他线程对此变量副本
        4. main方法 再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中

        加锁

        修改main方法

        // main方法while(true) {
        synchronized (t) {
        if(t.isFlag()){
        System.out.println("主线程进入循环执行~~~~~");
                }
            }
        }

        image.gif

        运行结果


        可以看到同样是解决了 共享变量不可见性的问题


        工作原理

          1. 某一个线程进入synchronized代码块前后,执行过程入如下:
          2. 线程获得锁
          3. 清空工作内存
          4. 从主内存拷贝共享变量最新的值到工作内存成为副本
          5. 执行代码
          6. 将修改后的副本的值刷新回主内存中
          7. 线程释放锁


          虽然加锁同样能解决 共享变量不可见性的问题,但是 加锁 和 锁的释放 过程都是会有性能消耗的,所以在解决 共享变量不可见性的问题 时,首选 volatile关键字


          volatile 关键字 -- 更深入的问题

          除了volatile可以保证可见性外,volatile 还具备如下一些突出的特性:

          • volatile的原子性问题volatile不能保证原子性操作。
          • 禁止指令重排序:volatile可以防止指令重排序操作。


          volatile不保证原子性

          原子性:在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行

          看如下程序,该程序开启了100个线程,同时对同一个变量进行自增10000次

          publicclassVolatileDemo04 {
          publicstaticvoidmain(String[] args) {
          // 1.创建一个线程任务对象Runnabletarget=newThreadTarget01();
          // 2.开始100个线程对象执行这个任务。for(inti=1 ; i<=100 ; i++ ) {
          newThread(target,"第"+i+"个线程").start();
                  }
              }
          }
          // 线程任务类classThreadTarget01implementsRunnable{
          // 定义一个共享变量privatevolatileintcount=0 ;
          @Overridepublicvoidrun() {
          synchronized (ThreadTarget01.class){
          for(inti=1 ; i<=10000 ; i++ ) {
          count++;
          System.out.println(Thread.currentThread().getName()+"count =========>>>> "+count);
                      }
                  }
              }
          }

          image.gif

          最后的结果正常应该是 1000000

          但是,实际上是有可能会少于 1000000 的

          但是我已经运行了好多次,没有出先少于的情况,所以没运行结果哈哈


          原理

          count++操作包含3个步骤:

          • 从主内存中读取数据到工作内存
          • 对工作内存中的数据进行++操作
          • 将工作内存中的数据写回到主内存


          count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断

          比如:

            1. 线程A从主存中读取count的值为100,此时由于CPU的切换关系,此时CPU的执行权被切换到了B线程,A线程就处于就绪状态,B线程处于运行状态
            2. 线程B也需要从主内存中读取count变量的值,由于线程A没有对count值做任何修改,因此此时B读取到的数据还是100
            3. 线程B工作内存中count执行了+1操作,但是未刷新到主内存中
            4. 此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主内存,因此A线程工作内存中的变量值还是100,没有失效。
            5. A线程对工作内存中的数据进行了+1操作
            6. 线程B101写入到主内存
            7. 线程A101写入到主内存


            虽然计算了2次,但是只对A进行了1次修改

            因此,在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)


            要保证原子性操作,有两种方法:1、使用锁机制 2、原子类 这里不在展开讲


            volatile禁止指令重排序

            重排序:

            为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序

            image.gif


            重排序虽然可以提高执行的效率,但是在并发执行下,JVM虚拟机底层并不能保证重排序下带来的安全性等问题,请看如下案例

            publicclassOutOfOrderDemo06 {
            // 新建几个静态变量publicstaticinta=0 , b=0;
            publicstaticinti=0 , j=0;
            publicstaticvoidmain(String[] args) throwsException {
            intcount=0;
            while(true){
            count++;
            a=0 ;
            b=0 ;
            i=0 ;
            j=0 ;
            // 定义两个线程。// 线程AThreadt1=newThread(newRunnable() {
            @Overridepublicvoidrun() {
            a=1;
            i=b;
                            }
                        });
            // 线程BThreadt2=newThread(newRunnable() {
            @Overridepublicvoidrun() {
            b=1;
            j=a;
                            }
                        });
            t1.start();
            t2.start();
            t1.join(); // 让t1线程优先执行完毕t2.join(); // 让t2线程优先执行完毕// 得到线程执行完毕以后 变量的结果。System.out.println("第"+count+"次输出结果:i = "+i+" , j = "+j);
            if(i==0&&j==0){
            break;
                        }
                    }
                }
            }

            image.gif

            正常情况下,会有以下三种情况

              • a = 1 ; i=b(0) ; b = 1 ; j = a(1) 最终(i = 0, j = 1)
              • b = 1 ; j=a(0) ; a = 1 ; i = b(1) 最终(i = 1, j = 0)
              • b = 1 ; a=1 ; i = b(1) ; j = a(1) 最终(i = 1, j = 1)


              但是,在很小的情况下会出现另外一种结果 i = 0 , j = 0

              这就是发生重排序的结果

              比如 线程1 中先执行了 i = b,然后切换到 进程2 且先执行 j = a,然后再分别执行 a = 1,b = 1

              这样输出的结果就是 i = 0 , j = 0

              而使用volatile可以禁止指令重排序,从而修正重排序可能带来的并发安全问题 ,如下

              publicclassOutOfOrderDemo07 {
              // 新建几个静态变量publicstaticinta=0 , b=0;
              publicvolatilestaticinti=0 , j=0;
              publicstaticvoidmain(String[] args) throwsException {
              intcount=0;
              while(true){
              count++;
              a=0 ;
              b=0 ;
              i=0 ;
              j=0 ;
              // 定义两个线程。// 线程AThreadt1=newThread(newRunnable() {
              @Overridepublicvoidrun() {
              a=1;
              i=b;
                              }
                          });
              // 线程BThreadt2=newThread(newRunnable() {
              @Overridepublicvoidrun() {
              b=1;
              j=a;
                              }
                          });
              t1.start();
              t2.start();
              t1.join(); // 让t1线程优先执行完毕t2.join(); // 让t2线程优先执行完毕// 得到线程执行完毕以后 变量的结果。System.out.println("第"+count+"次输出结果:i = "+i+" , j = "+j);
              if(i==0&&j==0){
              break;
                          }
                      }
                  }
              }

              image.gif

              目录
              相关文章
              |
              5月前
              |
              存储 Java 数据库
              Static关键字在Java中的多种用途解析
              Static关键字在Java中的多种用途解析
              |
              5月前
              |
              JavaScript 前端开发 开发者
              JavaScript中的const关键字解析
              JavaScript中的const关键字解析
              |
              4月前
              |
              存储 C语言
              C语言中static关键字的作用与用法解析
              C语言中static关键字的作用与用法解析
              |
              4月前
              |
              存储 Java 数据库
              Static关键字在Java中的多种用途解析
              Static关键字在Java中的多种用途解析
              |
              5月前
              |
              缓存 Java 编译器
              必知的技术知识:Java并发编程:volatile关键字解析
              必知的技术知识:Java并发编程:volatile关键字解析
              26 0
              |
              6月前
              |
              缓存 Java 编译器
              JMM内存模型 volatile关键字解析
              JMM内存模型 volatile关键字解析
              46 0
              |
              3天前
              |
              监控 Java 应用服务中间件
              高级java面试---spring.factories文件的解析源码API机制
              【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
              16 2
              |
              1月前
              |
              缓存 Java 程序员
              Map - LinkedHashSet&Map源码解析
              Map - LinkedHashSet&Map源码解析
              67 0
              |
              1月前
              |
              算法 Java 容器
              Map - HashSet & HashMap 源码解析
              Map - HashSet & HashMap 源码解析
              52 0
              |
              1月前
              |
              存储 Java C++
              Collection-PriorityQueue源码解析
              Collection-PriorityQueue源码解析
              60 0

              推荐镜像

              更多