彻底理解Java并发:volatile关键字

news2024/12/19 21:07:59

本篇内容包括:volatile 关键字简介、volatile 保证可见性(包括:关乎不可见性问题描述、JMM内存模型和不可见性的解决方案)以及 volatile 其他特性(包括:volatile 不保证原子性、volatile 原子性的保证操作、volatile 禁止指令重排、内存屏障和 happens-before 规则)

一、volatile 关键字简介

Java 中的 volatile 关键字,用来修饰会被不同线程访问和修改的变量,通常用于并发编程中,是 Java 虚拟机提供的轻量化同步机制。

volatile 关键字可以保证 JMM(Java内存模型)三个特征中的两个,即可见性与有序性。

volatile 关键字的两个作用:

  1. volatile 能够保证共享变量之间的可见性;
  2. volatile 禁止指令重排序。

二、volatile 保证可见性

1、关乎不可见性问题描述

首先要明确一点的是:只要直接采用了多线程的并发模型,并采用共享内存的方式作为数据的通讯方式,就一定有可见性问题。

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

从硬件层面上来讲,当 CPU 需要从主内存获取数据时,会拷贝一份到高速缓存中,CPU 计算时就可以直接在高速缓存中进行数据的读取和写入,提高吞吐量。当数据运行完成后,再将高速缓存的内容刷新到主内存中,此时其他 CPU 看到的才是执行之后的结果,但在这之间存在着时间差。

2、Java内存模型(JMM)

JMM(Java Memory Model):Java内存模型,是Java虚拟机规范中锁定义的一种内存模型,屏蔽掉了底层不同计算机的区别;

JMM 描述了Java程序中各种变量(线程共享变量)的访问规则,以及JVM中将变量存储到内存和从内存中读取变量这样的底层细节,JMM有以下规定:

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

本地内存和主内存的关系:

JMM的线程通信

3、不可见性的解决方案

从 JSR-133 开始(即从Jdk5开始),volatile 变量的写-读可以实现线程之间的通信。当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

即:volatile 会插入内存屏障以阻止重排序。

下面我们分别在 synchronized 和 volatile 的角度,去体验一下他们对于不可见性的解决方案的工作逻辑。

  1. 加锁,synchronized 的加锁逻辑:①、线程获取锁;②、线程清工作内存;③、线程从主内存重新拷贝共享变量最新值到工作内存副本;④、执行代码;⑤、修改后的值返回并刷新主内存;⑥、线程释放锁
  2. 加 volatile,工作逻辑:①、子线程T从主内存读取到数据放入对应的工作内存;②、子线程 T 将 flag 的修改位 true,但此时 flag 的值还没同步回主内存;③、主线程 main 方法读取到了 flag 的值为 flag;④、子线程T将值同步回主内存;⑤、主内存利用总线嗅探机制,失效其他线程对此变量的副本;⑥主线程在对 flag 进行操作的时候会从主线程读取最新的值,放入工作内存中。

三、volatile 其他特性

1、volatile 不保证原子性

原子性是指在一次操作或者多次操作中,要么所有的操作前部都得到了执行并且不会受到人格因素的干扰而中断,要么所有的操作都不执行,volttile 不保证原子性

以 COUNT++ 问题为例,操作包含三个步骤:

  1. 从主内存读取数据到工作内存
  2. 对工作内存中的数据进行 ++ 操作
  3. 将工作内存中的数据同步回主内存

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

2、volatile 原子性的保证操作

  • 加锁机制:我们给 count++ 操作添加锁,那么 count++ 操作就是临界区的代码,临界区代码同一时刻只能由一个线程去执行,即 count++ 变成了原子性操作
  • 使用原子类:从 Jdk1.5 开始提供了 java.util.countrrent.atomic 包,这个包中的原子操作类提供了一种用法简单,性能搞笑,线程安全地和更新变量的方式

3、volatile 禁止指令重排

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

原因:一个好的内存模型实际上会放松对处理器和编译器的规则束缚,也就是说软件技术和硬件技术都为一个目标服务:在不改变程序执行结果的前提下,尽可能提高执行效率。JMM对底层尽量减少约束,十七能后发挥自身优势,因此,在执行程序时,为提高性能,编译器和处理器常常会对指令集逆行重排序,一般重排序科一分为如下三种:

  1. 编译器优化的重排序,编译器在不该百年单线程程序语义的前提下,重新安排语句的执行顺序;
  2. 指令级并行的重排序,现代处理器采用额指令级并行技术来讲多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
  3. 内存系统的个重排序,由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的

重排序的好处:重排序可以提高程序处理速度

重排序的问题:单线程的重排序很简单,因为可以通过语义分析就能知道前后代码的依赖性,但是多线程就不一样了,多线程环境里编译器和CPU指令优化根本无法识别多个线程之间存在的数据依赖性。

volatile 解决不可见问题的方式就是插入内存屏障以阻止重排序:

  • (假设存在前后两个操作)当第二个操作是 volatile 写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后
  • (假设存在前后两个操作)当第一个操作是 volatile 读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前。

4、内存屏障

内存屏障分为两种:Load Barrier 和 Store Barrier 即读屏障和写屏障。内存屏障有两个作用:

  1. 阻止屏障两侧的指令重排序;
  2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效

对于 Load Barrier 来说,在指令前插入 Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;

对于 Store Barrier 来说,在指令后插入 Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

由于内存屏障的作用,避免了 volatile 变量和其它指令重排序、线程之间实现了通信,使得 volatile表 现出了锁的特性。

volatile 性能:volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

5、happens-before 规则

使用 happens-before 概念阐述了两个操作之间的内存可见性

happens-before 关系定义如下:

  1. 如果一个操作 happens-before 另一个操作,那么意味着第一个操作的结果对第二个操作可见,而且第一个操作的执行顺序将排在第二个操作的前面。
  2. 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须按照 happens-before 关系指定的顺序来执行。如果重排序之后的结果,与按照 happens-before 关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)

happens-before 规则如下:

  1. 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。

  2. 监视器锁规则:对一个监视器锁的解锁,happens-before 于随后对这个监视器锁的加锁。

  3. volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

  4. 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。

  5. 线程启动(start) 规则:如果 线程A 执行操作 ThreadB.start()(启动线程B),那么 线程A 的 ThreadB.start() 操作 happens-before 于 线程B 中的任意操作

  6. 线程终结(join)规则:如果 线程A 执行操作 ThreadB.join() 并成功返回,那么 线程B 中的任意操作 happens-before 于 线程A 从 ThreadB.join() 操作成功返回。

  7. 线程中断操作:对线程 interrupt() 方法的调用,happens-before 于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted() 方法检测到线程是否有中断发生。

  8. 对象终结规则:一个对象的初始化完成,happens-before 于这个对象的 finalize() 方法的开始。

happens-before 规则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,当我们了解并可以合理的运用 happens-before 规则后,就可以更好的写出线程安全的代码啦!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1711.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mysql-高级命令(1)和一些函数(悟已往之不谏,知来者之可追)

一、高级命令 1.查询某个字段和多个字段 select 某个字段(或者多个字段,中间以逗号间隔)from 表名; 2. 去除字段的重复值 3.and 表示(两个条件都需要满足) 4.or (两个条件满足一点)…

【Linux】软件包管理器 yum 与编辑器 vim 的基本使用

文章目录一、yum 背景知识1、商业生态2、开源生态3、软件生态本土化二、yum 的基本使用1、查看软件包2、安装软件3、卸载软件三、vim 的基本使用1、vim 的基本概念2、vim 的基本操作2.1 模式间切换2.2 光标定位2.3 文本复制2.4 文本编辑2.5 底行模式的操作四、简单 vim 配置2、…

SpringBoot+SpringCloud+Nutty打造分布式在线消息推送服务(实例)

文章目录前言技术架构效果图后端项目消息数据定义存储结构消息状态Nutty消息服务项目结构改动消息bean消息处理器消息转换pojo工具审核消息处理controller实现类服务调用前端连接代码初始化接受消息消息的展示效果总结前言 其实关于这个的话,我先前的几篇博文&…

css选择器

碎碎念:都是一些自己在学习过程中的一点体会,如果有什么不对的感谢大家指正一起学习! css选择器一、常用选择器二、属性选择器三、其他五、栗子1. * 通配符2. 空格 div p3. > 子选择器4. ~ 通用兄弟选择器5. 相邻兄弟选择器6. , 选择器分…

使用 Vercel 快速部署前端项目

Vercel:一键部署前端项目。 前端项目部署的问题 先来说下前端项目的部署,一般来说有以下几个步骤: 项目打包上传到服务器域名解析SSL 证书申请Nginx 配置CDN 加速 如果是公司的项目,打包之后的步骤一般有专门的运维人员负责&am…

C++:多态

文章目录一、多态的概念二、多态的定义及实现2.1 多态的构成条件2.2 虚函数2.3 虚函数的重写(覆盖)2.4 override 和 final2.5 重载、覆盖(重写)、隐藏(重定义)的对比三、抽象类四、继承和多态常见的面试问题1.2.总结一、多态的概念 多态按字面的意思就是多种形态。当类之间存在…

瞪羚优化算法(Gazelle Optimization Algorithm,GOA)

瞪羚优化算法(Gazelle Optimization Algorithm,GOA)由Agushaka等人于2022年提出,该算法模拟了瞪羚逃避捕食者的行为,思路新颖,性能高效。 瞪羚的身高60-110厘米,体重13-29千克。该属物种有像小鹿…

【Linux】基本指令(上)

​🌠 作者:阿亮joy. 🎆专栏:《学会Linux》 🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根 目录👉操作系统&…

Linux - 第2节 - Linux环境基础开发工具使用

1.Linux 软件包管理器 yum centos 7中安装软件的方式: (1)源码安装 (2)rpm包安装 (3)yum安装 yum安装的好处: (1)不用编译源码 (2)不用…

赶紧进来看看---C语言实现学生信息管理系统(2.0动态内存版)

本文介绍了将学生信息管理系统静态版本改造为动态内存版本,主要涉及改造思路,枚举类型的使用,动态内存函数和柔性数组的使用,动手写程序才能使基础知识更为牢固…(文章最后有源码展示) 学生信息管理系统1.0静态版->学生信息管理系统2.0动态内存版 c语言实现学生信息管理系统…

在python中使用ggplot2

python的ggplot2库:plotnine > 一.安装方法: pip install plotnine使用的编译器:pycharm 二.plotnine绘图 1.第一个图形 除了导包的操作不一致,其他类似 from plotnine import ggplot, geom_point, aes, stat_smooth, facet_wrap fr…

70.【JavaScript 6.0】

前端三要素1.前端三要素2.引入JavaScript3.JavaScript基本语法入门4.数据类型5.严格检查模式( use strict)6.Map和Set7.函数的定义和参数获取8.变量的作用域(局部 全局)-------> 和Java一样9.全局规范:10.方法的定义和调用11.内部对象1.Date--------->日期2.JSON--------…

自定义mybatis插件实现sql日志打印

自定义mybatis插件实现sql日志打印 mysql插件实现原理 官网的关键信息 参考文档 https://mybatis.org/mybatis-3/zh/configuration.html#plugins 官方文档 MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方…

【JavaSE】一篇文章领悟Java运算符

前言: 作者简介:爱吃大白菜1132 人生格言:纸上得来终觉浅,绝知此事要躬行 如果文章知识点有错误的地方不吝赐教,和大家一起学习,一起进步! 如果觉得博主文章还不错的话,希望三连支持&#xff01…

Java项目:超市管理系统(java+SSM+JSP+LayUI+jQ+Mysql)

源码获取:俺的博客首页 "资源" 里下载! 项目介绍 本项目分为超级管理员、总经理、店长、员工等角色,超级管理员可添加修改删除角色并进行角色菜单配置; 超级管理员角色包含以下功能: 商品管理:添…

C语言高级-4栈

14天阅读挑战赛 目录 一、栈的原理 1、栈的定义 2、栈的应用 (1)选课问题 (2)旅游:怎么样把每个城市去且仅去一遍? (3)栈的使用场景 (4)思考&#xf…

C++多态之虚函数表详解及代码示例

引言 C相对其他面向对象语言来说,之所以灵活、高效。很大程度的占比在于其多态技术和模板技术。C虚函数表是支撑C多态的重要技术,它是C动态绑定技术的核心。 如果对多态还不了解的小伙伴,可以点这里C多态详解基础篇。 在不考虑继承的情况下…

Vue3 Hooks 模块化抽离

Vue3中的Hooks 其实就是业务逻辑的抽离,跟Vue2中mixin 本质上是一样的:将当前组件的业务逻辑抽离到一个公共的文件中,提高逻辑复用性,让当前组件看起来更加清爽,不太一样的地方是我们封装hooks 的时候一般是返回一个函…

如何不改动 GatewayWorker 依赖包下自定义协议

前言: GatewayWorker 是 Workerman 的一个框架,对应用层开发者更友好。GatewayWorker 多了一个网关,也就是 Gateway,负责与客户端连接,消息转发等。而自定义的协议,也就是 gateway 面向客户端提供服务的协议…

java毕业设计——基于java+JDBC+sqlserver的物业管理系统设计与实现(毕业论文+程序源码)——物业管理系统

基于javaJDBCsqlserver的物业管理系统设计与实现(毕业论文程序源码) 大家好,今天给大家介绍基于javaJDBCsqlserver的物业管理系统设计与实现,文章末尾附有本毕业设计的论文和源码下载地址哦。 文章目录: 基于javaJDB…