什么是Volatile关键字?

什么是Volatile关键字?一、Java的内存模型(JMM)在仔细讲解Java的volatile关键字之前有必要先了解一下【Java的内存模型】Java的内存模型简称JMM(JavaMemoryModel),是Java虚拟机所定义的一种抽象规范用来屏蔽【不同硬件】和【操作系统】的【内存访问差异】。让Java程序在各种平台下都能达到一致的内存访问效果。…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

一、Java的内存模型(JMM)

          在仔细讲解Java的volatile关键字之前有必要先了解一下【Java的内存模型

          Java的内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范用来屏蔽【不同硬件】和【操作系统】的【内存访问差异】

          让Java程序在各种平台下都能达到一致的内存访问效果。

          Java内存模型长成什么样子呢?
             就是下图的样子:

             什么是Volatile关键字?

           这里需要解释几个概念:

               ①. 主内存(Main Memory)

                       主内存可以简单理解为计算机当中的内存,但又不完全等同。

                       主内存被所有线程共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的”本尊”。

               ②. 工作内存(Working Memory)

                       工作内存可以理解为计算机当中的CPU高速缓存,但又不完全等同。

                       每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的”副本”。

          线程对【共享变量】的所有操作都必须在【工作内存】中进行不能直接读写【主内存】中的变量

          不同线程之间也无法访问彼此的【工作内存】,【变量值的传递】只能通过【主内存】来进行。

          直接操作【主内存】太慢,所以JVM才不得不利用性能较高的【工作内存】。

          这里类比一下:CPU、高速缓存、内存 之间的关系

        【工作内存】所更新的【变量】并不会立即同步到主内存。

           

二、Volatile关键字具有的特性

      ①. volatile关键字具有许多特性,其中最重要的特性就是保证了用volatile修饰的变量对所有线程的可见性

          这里的可见性是什么意思呢?

             当一个线程修改了变量的值,新的值会立刻同步到主内存当中。
             而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。

         为什么volatile关键字可以有这样的特性?

             因为Java语言的【先行发生原则(happens-before)

 

三、什么是先行发生原则(happens-before)?

          ①. 在计算机科学中,【先行发生原则】是两个事件的结果之间的关系
                如果一个事件发生在另一个事件之前,结果必须反映,即使这些事件实际上是乱序执行的(通常是优化程序流程)。

                  这里所谓的事件,实际上就是各种指令操作,比如读操作、写操作、初始化操作、锁操作等等。

         

          ②. 先行发生原则作用于很多场景下,包括同步锁、线程启动、线程终止、volatile。

                  我们这里只列举出volatile相关的规则:

                   对于一个volatile变量的写操作先行发生于后面对这个变量的读操作

                  如果在静态变量s之前加上volatile修饰符:
                    volatile static int s = 0;

                    线程A执行如下代码:
                       s = 3;

                   这时候我们引入线程B,执行如下代码:
                     System.out.println(“s=” + s);

                  当线程A先执行的时候,把s = 3写入主内存的事件必定会先于读取s的事件。所以线程B的输出一定是s = 3。

 

四、Volatile只能保证变量的【可见性】,并不能保证变量的【原子性】


   public class VolatileTest {

    public volatile static int count = 0;

    public static void main(String [] args){
        //开启10个线程
      for(int i = 0;i < 10; i++){
         new Thread(new Runnable() {

             @Override
             public void run() {
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 //每个线程中让count的值自增100次
                 for(int j = 0;j < 100;j++){
                     count++;
                 }
             }
         }).start();
      }

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count= " + count);
    }
}

 

这段代码是什么意思呢?

   很简单,开启10个线程,每个线程当中让静态变量count自增100次。执行之后会发现,最终count的结果值未必是1000,有可能小于1000。

   使用volatile修饰的变量,为什么并发自增的时候会出现这样的问题呢?

       这是因为count++这一行代码本身并不是原子性操作,在字节码层面可以拆分成如下指令: 
         getstatic     //读取静态变量(count)
         iconst_1     //定义常量1
         iadd            //count增加1
         putstatic     //把count结果同步到主内存 

虽然每一次执行 getstatic 的时候,获取到的都是主内存的最新变量值,但是进行iadd的时候,由于并不是原子性操作,其他线程在这过程中很可能让count自增了很多次。

这样一来本线程所计算更新的是一个陈旧的count值,自然无法做到线程安全:

什么是Volatile关键字?

什么是Volatile关键字?

什么是Volatile关键字?

什么是Volatile关键字?

 

五、什么时候适合用Volatile呢?

           1、运行结果并不依赖【变量的当前值】,或者能够确保只有单一的线程修改变量的值。

                  这个很好理解,就是上面的代码例子。

           2、变量不需要与其他的状态变量共同参与不变约束。

                 这个是什么意思呢?可以看看下面这个场景:

                    volatile static int start = 3;

                    volatile static int end = 6;

                    线程A执行如下代码:

                   while (start < end){

                       //do something

                   }

                  线程B执行如下代码:

                  start+=3;

                  end+=3;

这种情况下,一旦在线程A的循环中执行了线程B,start有可能先更新成6,造成了一瞬间 start == end,从而跳出while循环的可能性。

 

六、Volatile对【指令的重排序】影响

          1)什么是【指令重排序】?

                  指令重排序是指: JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的【指令顺序】进行【重新排序】

          2)指令重排序的【目的】?

                  目的:为了在不改变程序执行结果的前提下,优化程序的运行效率。

                             需要注意的是,这里所说的不改变执行结果,指的是【单线程】下的程序执行结果

          3)然而,指令重排序是一把双刃剑,虽然优化了程序的执行效率,但是在某些情况下,会影响到【多线程】的执行结果。

                我们来看看下面的例子:

                    boolean contextReady = false;

                   在线程A中执行:

                   context = loadContext();

                   contextReady = true;

                   在线程B中执行:

                    while( ! contextReady ){ 

                     sleep(200);

                    }

                   doAfterContextReady (context);    

以上程序看似没有问题。线程B循环等待上下文context的加载,一旦context加载完成,contextReady == true的时候,才执行doAfterContextReady 方法。

但是,如果线程A执行的代码发生了指令重排,初始化和contextReady的赋值交换了顺序:

boolean contextReady = false;

在线程A中执行:

contextReady = true;

context = loadContext();

 

在线程B中执行:

while( ! contextReady ){ 

   sleep(200);

}

doAfterContextReady (context);

这个时候,很可能context对象还没有加载完成,变量contextReady 已经为true,线程B直接跳出了循环等待,开始执行doAfterContextReady 方法,结果自然会出现错误。

需要注意的是,这里java代码的重排只是为了简单示意,真正的指令重排是在【字节码指令层面】

 

七、指令重排序解决方法?

           【内存屏障】

        1)什么是内存屏障?

                内存屏障(Memory Barrier)是一种CPU指令

              内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。

              这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。

 

内存屏障共分为四种类型:

  1) LoadLoad屏障:

           抽象场景:Load1; LoadLoad; Load2

           Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

  2)StoreStore屏障:

           抽象场景:Store1; StoreStore; Store2

           Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见

  3)LoadStore屏障:

           抽象场景:Load1; LoadStore; Store2

           在Store2被写入前,保证Load1要读取的数据被读取完毕。

  4)StoreLoad屏障:

          抽象场景:Store1; StoreLoad; Load2

          在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的。

 

八、内存屏障在Java代码中怎么使用?

           这就涉及到了【Volatile】,来看看它究竟为我们做了些什么。

       volatile做了什么?

          在一个变量被Volatile修饰后,JVM会为我们做2件事:

            1.  在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。

            2.  在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。

         或许这样说有些抽象,我们看一看刚才线程A代码的例子:

            boolean contextReady = false;

           在线程A中执行:

           context = loadContext();

           contextReady = true;

     我们给contextReady 增加volatile修饰符,会带来什么效果呢?

       什么是Volatile关键字?

         由于加入了StoreStore屏障,屏障上方的普通写入语句 context = loadContext()  和屏障下方的volatile写入语句 contextReady = true 无法交换顺序,从而          成功阻止了指令重排序。

      什么是Volatile关键字?

  那么内存屏障和之前所介绍的Java语言happens-before规则之间,是什么样的关系呢?

     happens-before是JSR-133规范之一,内存屏障是CPU指令。

     可以简单认为前者是最终目的,后者是实现手段。

 

九、总结

         1.  Volatile特性之一:

                 保证【变量】在【多线程】之间的【可见性】。

                 可见性的保证是基于CPU的内存屏障指令,被JSR-133抽象为happens-before原则。

         2. Volatile特性之二:

                 阻止编译时和运行时的指令重排。

                 编译时JVM编译器遵循【内存屏障】的约束

                 运行时依靠【CPU屏障指令】来阻止重排。

几点补充:

1. 关于volatile的介绍,本文很多内容来自《深入理解Java虚拟机》这本书。有兴趣的同学可以去看看。

 

2. 在使用volatile引入内存屏障的时候,普通读、普通写、volatile读、volatile写会排列组合出许多不同的场景。我们这里只简单列出了其中一种,有兴趣的同学可以查资料进一步学习其他阻止指令重排的场景。

 

3.volatile除了保证可见性和阻止指令重排,还解决了long类型和double类型数据的8字节赋值问题。这个特性相对简单,本文就不详细描述了。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/157712.html原文链接:https://javaforall.net

(0)
上一篇 2022年7月27日 下午2:16
下一篇 2022年7月27日 下午2:36


相关推荐

  • 大数据管理平台(一)概述「建议收藏」

    大数据管理平台(一)概述「建议收藏」系列文章目录文章目录系列文章目录前言一、功能概述二、使用步骤2.1安装2.2监控2.3管理2.4用户管理2.5应用市场2.6操作系统三、总结前言做大数据有几年了,这些年耳濡目染了一些大数据管理平台的使用,但是或多或少使用起来,都不怎么方便,所以决定自己来实现一个简单的大数据平台一、功能概述大数据应用组件往往很多,可能几百台服务器组成一个Hadoop集群,当部署这些节点时,需要一个节点一个节点的操作,简直不敢想象。同时在这些服务器上可能还部署着Spark、Flink、Hive

    2022年5月1日
    51
  • 尽管Grok引发争议,马斯克的xAI又获得了200亿美元的资金

    尽管Grok引发争议,马斯克的xAI又获得了200亿美元的资金

    2026年3月15日
    2
  • JMeter入门教程详解

    JMeter入门教程详解简介本文由 xmeter 君写给想了解性能测试和 JMeter 的小白 适合对这两者了解很少的同学们 如果已经有使用经验的请绕道 别浪费时间 我们将介绍 JMeter 的使用场景 如何安装 运行 JMeter 以及开始一个最最简单的测试 你还徘徊在 JMeter 的门口吗 别犹豫了 跟着本文做完 你就可以开启你的 JMeter 之旅了 JMeter 是开源软件 Apache 基金会下的一个性能测试工具 用来测试部署在服务器端的应用程序的性能 近来 JMeter 因为其使用简单 现在也被社区作为接口测试工具 啥 什么是性能测

    2026年3月20日
    2
  • 深度学习中的gelu激活函数详解

    深度学习中的gelu激活函数详解论文 gaussianerro 一 什么是 gelus 激活函数 gelus gaussianerro 就是我们常说的高斯误差线性单元 它是一种高性能的神经网络激活函数 公式如下 1 xP X x x x xP X lex x Phi x tag 1 xP X x x x 1 计算结果大约为 2 0 5x 1

    2026年3月18日
    2
  • 一叶知秋,一个 LED 就能入门 Linux 内核「建议收藏」

    一叶知秋,一个 LED 就能入门 Linux 内核「建议收藏」简单点灯最近项目上需要用到LED子系统,在嵌入式Linux里面点个灯还是比较简单的,只要在某个灯对应的目录里,向相应文件写入特定值,就可以让LED亮/灭/闪烁。#echo1>/sys/class/leds/green/brightness//点亮LED#echo0>/sys/class/leds/green/brightness//熄灭LED#echoheartbeat>/sys/class/leds/green/trigger//

    2022年7月16日
    22
  • 面向对象数据库简介

    面向对象数据库简介面向对象数据的定义面向对象数据库 OODB 是一个基于面向对象编程语言 OOP 的数据库 其数据都是以对象 类的形式表示并存储在面向对象数据库中 简单来讲 面向对象数据库 面向对象编程语言 关系型数据库特性 在这个公式里面 面向对象编程语言的三个特性为继承 多态 封装 而关系型数据库特性的三个特性 实体完整性 并发 查询处理 因此可用下图 1 表示面向对象数据库 关系型数据库的问题

    2026年3月20日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号