volatile 关键字 (详细解析)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 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

              目录
              相关文章
              |
              13天前
              |
              存储 缓存 Java
              Java 并发编程——volatile 关键字解析
              本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
              Java 并发编程——volatile 关键字解析
              |
              1月前
              |
              Java 程序员
              面试官的加分题:super关键字全解析,轻松应对!
              小米,29岁程序员,通过一个关于Animal和Dog类的故事,详细解析了Java中super关键字的多种用法,包括调用父类构造方法、访问父类成员变量及调用父类方法,帮助读者更好地理解和应用super,应对面试挑战。
              41 3
              |
              19天前
              |
              安全 编译器 C++
              C++ `noexcept` 关键字的深入解析
              `noexcept` 关键字在 C++ 中用于指示函数不会抛出异常,有助于编译器优化和提高程序的可靠性。它可以减少代码大小、提高执行效率,并增强程序的稳定性和可预测性。`noexcept` 还可以影响函数重载和模板特化的决策。使用时需谨慎,确保函数确实不会抛出异常,否则可能导致程序崩溃。通过合理使用 `noexcept`,开发者可以编写出更高效、更可靠的 C++ 代码。
              25 0
              |
              5月前
              |
              网络协议 Java 数据库连接
              13 Java异常(异常过程解析、throw、throws、try-catch关键字)
              13 Java异常(异常过程解析、throw、throws、try-catch关键字)
              128 2
              |
              6月前
              |
              存储 C语言
              C语言中static关键字的作用与用法解析
              C语言中static关键字的作用与用法解析
              |
              6月前
              |
              存储 Java 数据库
              Static关键字在Java中的多种用途解析
              Static关键字在Java中的多种用途解析
              |
              2月前
              |
              监控 Java 应用服务中间件
              高级java面试---spring.factories文件的解析源码API机制
              【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
              87 2
              |
              3月前
              |
              缓存 Java 程序员
              Map - LinkedHashSet&Map源码解析
              Map - LinkedHashSet&Map源码解析
              87 0
              |
              12天前
              |
              存储 设计模式 算法
              【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
              行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
              【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
              |
              12天前
              |
              设计模式 存储 安全
              【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
              结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
              【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析

              推荐镜像

              更多