当前位置: 首页 > news >正文

Java中的volatile到底是什么来路

今天来介绍下Java中的volatile关键字,volatile也是Java并发编程中不得不看的一个部分,在之前的文章中说到的双重检测单例模式其实也跟volatile也有不解之缘,另外volatile跟计算机体系架构(CPU)也是有着千丝万缕的关系。

所以引入volatile之前,还是先来理解下背景:

由于计算的cpu、内存、IO设备的速度不均衡,cpu的速度最快,内存其次,IO设备速度最慢。因此在数据传输的过程中,就会出现cpu等待内存数据,内存等待IO数据的情况,整体上就会导致CPU不能高效利用,浪费资源。

于是科学家们为了提高CPU的利用率,不让CPU闲置,从操作系统层面和软件层面进行优化,比如之前同步?阻塞?到底什么是I/O模型这篇文章中提到的各种I/O模型,其目的就是为了越来越高效地利用CPU;此外,在CPU中,除了存在寄存器之外,还设置了一级、二级、三级...缓存,其目的也是为了缓解CPU和内存速度的差异,CPU可以优先从缓存中进行获取数据,这样不必每次都要去访问内存,也是一种优化方式。

但是事情往往总是两面性的,在优化性能和速度的同时,又引入了新问题,在一台主机具备多CPU多核的时代,计算机上的程序往往都是多用户多进程多线程式地去跑,在一台具有多CPU多核的机器上,程序是可以达到真正地并行计算的,而问题也就随之而来.......

下面是一段经典的多线程使用共享变量的例子:

int a = 0;
// 线程A运行a += 1
// 线程B运行a += 1

这里的共享变量a被线程A和线程B共享,可以试想下,最后a的结果是多少呢?

如果是单线程运行,毫无疑问,a的结果肯定是2,而在多线程的情况下,结果是多样的,有可能会出现a=1的情况,因为线程A和线程B是运行在一台2个CPU的机器上,线程A和线程B各占一个CPU,那也就是说线程A和线程B会各自享有自己占据的那个CPU上的寄存器和缓存,CPU在计算的时候会把要计算的数值取到寄存器中,计算完,会刷新到缓存或者主存(主存即内存)中,而当两个CPU各自把a这个变量加载进寄存器中后,会进行各自的计算,只要没有把CPU中寄存器中的值同步进内存中,那么两个线程中缓存中的a的值对双方来说都是不可见的,因此就如下图所示情况:

当线程A做完第1步操作:a=a+1. 这时候a的值还没有写到内存,而线程B也开始了a=a+1的操作,然后线程A将a的值写入内存,最后线程B也将a的值写入内存,所以a的值最终还是为1。

所以看到这里,可以看出来多线程情况下出现的问题,其实也是因为一系列的优化问题引起的,而在Java中要解决这个问题,Java也提供了相应的措施给开发者,比如今天要提到的volatile,还有诸如原子操作,锁操作等等。关于上面的那个例子,不得不再多一句最,即使用volatile来修饰变量a,其实也不能完全保证a可以得到正确的值,因为volatile仅仅保证的是内存可见性,而不是原子性,关于volatile保证内存可见性的问题下面会进行介绍。

今天主要介绍volatile这个关键字,通过对它做了一些解读和研究,主要分为这几点:

  1. 指令集重排序优化

  2. volatile保证内存可见性

  3. JMM(Java Memory Model模型),happens-before 规则

这几个点其实也是比较绕口的专业术语,也涉及到了JMM(Java Memory Model)的知识点,下面还是看例子,比较有清晰的认识:

以单例模式这个例子说起,单例模式中有一种写法是懒汉模式,并且为了多线程安全以及性能优化的考虑,用了双重检测的机制,在之前的文章单例模式(三)和单例模式(四)也讲过,看下面这段代码:

public class Singleton {    private static Singleton singleton;    private Singleton(){    }    public static Singleton getInstance(){        if (singleton == null){            synchronized (Singleton.class){                if(singleton == null)                    singleton = new Singleton();            }        }        return singleton;    }}

针对这种双重检测,更加严谨的做法就是在静态成员变量singleton 的前面加上volatile这个关键字,因为如果不加这个volatile可能会到导致多线程访问单例对象singleton的时候会发生报错,因为从高级语言层面来理解代码的执行是很难去想通这个逻辑的,因为Java会被编译为Class文件,就是字节码文件,字节码文件中包含的是一条条的指令,指令是由java虚拟机去解释执行的,所以这里的singleton = new Singleton();这句初始化语句其实被分解为下面几条语句:

new           #3                  // class cs/designpattern/singleton/bean/Singleton2...invokespecial #4                  // Method "<init>":()Vputstatic     #2                  // Field singleton:Lcs/designpattern/singleton/bean/Singleton2;

可以看到主要的指令执行就是:

  1. new对象

  2. 调用构造函数

  3. 引用赋值

而这三句指令目前的执行顺序是没有问题的,但是问题在于指令执行过程中,根据不同的计算机体系架构,CPU会对其进行指令集优化,当CPU优化指令集的时候,会对上面的指令集进行一种重排序的操作,而这种重排序的操作就会导致上面的指令顺序可能会变为:

  1. new对象

  2. 引用赋值

  3. 调用构造函数

这时候,会发生原本在第3步的引用赋值先于第2步的构造函数的调用,这时候就有可能发生这样的错误:假设这时候A、B两个线程来共同执行这端逻辑,A已经获取了锁,进入了锁里面的代码段,并且执行了1、2这两句指令操作,然后当B线程过来的时候,发现instance不为空了,所以就直接返回,而这时候A其实还没有执行第3个指令,所以B获取的instance其实就是一个未进行初始化(没有调用构造函数)的对象,这时候,如果使用这个对象就会报错。而引入volatile就是为了解决这个问题,代码如下:

public class Singleton {    private static volatile Singleton singleton;    private Singleton(){    }    public Singleton getInstance(){        if (singleton == null){            synchronized (Singleton.class){                if(singleton == null)                    singleton = new Singleton();            }        }        return singleton;    }}

volatile在这里的语义就是防止指令重排序,这样在初始化对象的时候,就能强制保证如下的顺序,保证了引用赋值不会先于构造函数的调用:

  1. new对象

  2. 调用构造函数

  3. 引用赋值

所以B线程就不会再获取一个未初始化过的对象了。

关于volatile防止指令重排序,其实也是JMM规定的happens-before规则之一, 在happens-before的规范中,volatile的一种语义就是防止指令重排序。

看完指令重排序后,来看volatile的另外一种语义:内存可见性

仍然来看一个小例子:

volatile boolean stop = false;// 线程A执行void shutdown(){    stop = true}
// 其他线程执行while(stop){  stopCurrentThread();

这里我们可以看到将stop这个多线程共享变量设置了volatile属性,为的就是可以立即让stop这个变量的值刷新到内存中,从而让其他线程执行的时候可以立即感知到stop这个变量的值发生了变,所以这里volatile的可见性语义其实就是体现在可以及时地将CPU寄存器或者CPU缓存中的计算结果及时地刷新到内存,起到多线程可以感知的效果,这也是JVM内存模型中happens-before定义的一个规则。

总结一下,今天主要记录了Java中volatile的两种语义:

  1. 防止指令重排序

  2. 内存可见性

关于JMM中的happens-before的规则,只是略微提了下,后面有时间也会做相应地记录和分析。

http://www.xdnf.cn/news/1079533.html

相关文章:

  • RAG实战指南 Day 4:LlamaIndex框架实战指南
  • CentOS系统高效部署fastGPT全攻略
  • 21、MQ常见问题梳理
  • 【论】电力-交通融合网协同优化:迎接电动汽车时代的挑战
  • thinkphp8接管异常处理类
  • 【第三章:神经网络原理详解与Pytorch入门】01.神经网络算法理论详解与实践-(2)神经网络整体结构
  • STM32-第二节-GPIO输入(按键,传感器)
  • C盘爆满元凶!WinSxS组件解密
  • JsonCpp的核心类及核心函数使用汇总
  • Web 服务器架构选择深度解析
  • Linux常见指令以及权限理解
  • Flowable12基础流程实战资金申请------------持续更新中
  • 埃及黑白沙漠:2亿年风蚀岩的“外星登陆现场“
  • 未来之窗冥界调试工具—东方仙盟
  • LTspice仿真10——电容
  • A模块 系统与网络安全 第四门课 弹性交换网络-1
  • 在小程序中实现实时聊天:WebSocket最佳实践
  • Verilog 语法介绍 1-1结构
  • Spring Boot + 本地部署大模型实现:基于 Ollama 的集成实践
  • KMP(Kotlin Multiplatform)改造(Android/iOS)老项目
  • PHP语法基础篇(八):超全局变量
  • 转录组分析流程(零):流程介绍
  • 【二分】-----【Music Notes S】
  • 【Git】同时在本地使用多个github账号进行github仓库管理
  • 通过Curtain 解决方案保障BIM模型安全共享—建筑业的防泄密实战
  • react-打包和本地预览 ——打包优化
  • 【数据结构】C++的unordered_map/set模拟实现(开散列(哈希桶)作底层)
  • npm 命令入门指南(前端小白版)
  • contenteditable网页富文本编辑无法选中图片
  • 从0到1实战!用Docker部署Qwerty Learner输入法的完整实践过程