volatile关键字为何能保证可见性和有序性以及变量自增线的程不安全问题

Java+之+volatile+详解 Volatile可以看做是轻量级的 Synchronized,它只保证了共享变量的可见性。在线程 A 修改被 volatile 修饰的共享变量之后,线程 B 能够读取到正确的值。java 在多线程中操作共享变量的过程中,会存在指令重排序与共享变量工作内存缓存的问题。 阅读详情

1. volatile简介

在上一篇文章中我们深入理解了java关键字,我们知道在java中还有一大神器就是关键volatile,可以说是和synchronized各领风骚,其中奥妙,我们来共同探讨下。

通过上一篇的文章我们了解到synchronized是阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁。而volatile就可以说是java虚拟机提供的最轻量级的同步机制。但它同时不容易被正确理解,也至于在并发编程中很多程序员遇到线程安全的问题就会使用synchronized。java内存模型告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。线程在工作内存进行操作后何时会写到主内存中?这个时机对普通变量是没有规定的,而针对volatile修饰的变量给java虚拟机特殊的约定,线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。

现在我们有了一个大概的印象就是:被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。

2. volatile实现原理

volatile是怎样实现了?比如一个很简单的Java代码:

在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令(具体的大家可以使用一些工具去看一下,这里我就只把结果说出来)。我们想这个Lock指令肯定有神奇的地方,那么Lock前缀的指令在多核处理器下会发现什么事情了?主要有这两个方面的影响:

  1. 将当前处理器缓存行的数据写回系统内存;
  2. 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。因此,经过分析我们可以得出如下结论:

  1. Lock前缀的指令会引起处理器缓存写回内存;
  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
  3. 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。

这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。

3. volatile的happens-before关系

经过上面的分析,我们已经知道了volatile变量可以通过缓存一致性协议保证每个线程都能获得最新值,即满足数据的“可见性”。我们继续延续上一篇分析问题的方式(我一直认为思考问题的方式是属于自己,也才是最重要的,也在不断培养这方面的能力),我一直将并发分析的切入点分为两个核心,三大性质。两大核心:JMM内存模型(主内存和工作内存)以及happens-before;三条性质:原子性,可见性,有序性(关于三大性质的总结在以后得文章会和大家共同探讨)。废话不多说,先来看两个核心之一:volatile的happens-before关系。

在六条happens-before规则中有一条是:**volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。**下面我们结合具体的代码,我们利用这条规则推导下:

public class VolatileExample {
    private int a = 0;
    private volatile boolean flag = false;
    public void writer(){
        a = 1;          //1
        flag = true;   //2
    }
    public void reader(){
        if(flag){      //3
            int i = a; //4
        }
    }
}

上面的实例代码对应的happens-before关系如下图所示:
在这里插入图片描述

加锁线程A先执行writer方法,然后线程B执行reader方法图中每一个箭头两个节点就代码一个happens-before关系,黑色的代表根据程序顺序规则推导出来,红色的是根据volatile变量的写happens-before 于任意后续对volatile变量的读,而蓝色的就是根据传递性规则推导出来的。这里的2 happen-before 3,同样根据happens-before规则定义:如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B的执行顺序,我们可以知道操作2执行结果对操作3来说是可见的,也就是说当线程A将volatile变量 flag更改为true后线程B就能够迅速感知。

4. volatile的内存语义

假设线程A先执行writer方法,线程B随后执行reader方法,初始时线程的本地内存中flag和a都是初始状态,下图是线程A执行volatile写后的状态图。
在这里插入图片描述
当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。下图就展示了线程B读取同一个volatile变量的内存变化示意图。
在这里插入图片描述
从横向来看,线程A和线程B之间进行了一次通信,线程A在写volatile变量时,实际上就像是给B发送了一个消息告诉线程B你现在的值都是旧的了,然后线程B读这个volatile变量时就像是接收了线程A刚刚发送的消息。既然是旧的了,那线程B该怎么办了?自然而然就只能去主内存去读取。

4.1 volatile的内存语义实现

我们都知道,为了性能优化,JMM在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序,那如果想阻止重排序要怎么办了?答案是可以添加内存屏障。
在这里插入图片描述
Java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:
在这里插入图片描述
"NO"表示禁止重排序。为了实现volatile内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM采取了保守策略:

  1. 在每个volatile写操作的前面插入一个StoreStore屏障;
  2. 在每个volatile写操作的后面插入一个StoreLoad屏障;
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障;
  4. 在每个volatile读操作的后面插入一个LoadStore屏障。

需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障

StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;

StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序

LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序

LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序
在这里插入图片描述
在这里插入图片描述

5.1 volatile型变量自增操作的隐患

对一个volatile字段进行自增操作,但这个字段不是原子类型的。如果多个线程同时对这个字段进行自增操作,可能会丢失数据。

olatile类型变量每次在读取的时候,会越过线程的工作内存,直接从主存中读取,也就不会产生脏读。那为何FindBugs报这个错?

根本原因在于++自增操作。Java的++操作对应汇编指令有三条
  1. 从主存读取变量值到cpu寄存器
  2. 寄存器里的值+1
  3. 寄存器的值写回主存

当声明一个volatile变量

volatile int a = 0;
a++;

编译后的操作会类似于有一个中间虚拟变量保存自增的结果

int a = 0;           //1
int temp = a + 1;    //2
return temp;            //3

在多线程情况下,假设存在线程a、b同时调用a++
线程a从步骤1执行到步骤2,
此时线程a对应的cpu缓存中: a = 0, temp = 1
线程b从步骤1执行到步骤3
此时线程b对应的cpu缓存中: a = 1, temp = 1

线程3将temp的值回写到主内存的变量a中,
此时线程a中temp值并非volatile 不会触发内存屏障中的缓存一致性协议,会直接将值回写到主内存中,导致线程b的值被覆盖,发生线程不安全问题。使用a = a+1;即可

参考文章

并发编程——为什么volatile变量++操作线程不安全

彻底理解volatile关键字

面试官:volatile是如何保证可见性和有序性的? volatile保证可见性的原理 可见性问题 可见性问题指的是一个线程在访问一个共享变量的时候,其他线程对该共享变量的修改对于第一个线程来说是不可见的,下面通过一个例子可以发现可见性问题。 public class Visable { private static boolean flag = true; public static void main(String[] args) throws InterruptedException { new Thread(() 阅读详情

相关推荐

线程安全的三大特性(原子性、可见性、有序性)、volatile关键字

volatile关键字介绍一:线程安全的三大特性1、原子性2、可见性3、有序性二:volatile关键字 一:线程安全的三大特性 1、原子性 原子性是指操作是不可分的。其表现在于对于共享变量的某些操作,应该是不可分的,必须连续完成。 比如 a++ 操作,实际上 JMM 会分 3 步来完成。 ①②③读取变量 xttblog 的值 xttblog 的值+1 将值赋予变量 xttblog 这三个操作中任何一个操作过程中,xttblog 的值被人篡改,那么都会出现我们不希望出现的结果。所以我们必须保证这是原子性的

小小本科生 9344

volatile型变量自增操作的隐患

用FindBugs跑自己的项目,爆出两处An increment to a volatile field isn’t atomic。相应报错的代码如下:volatile int num = 0; num++;  FindBugs针对这种类型的错误给出了相应的解释 An increment to a volatile field isn’t atomic This code incremen

bruce128的专栏 5165

为什么volatile关键字保证不了线程安全

在当前高并发的时代,不懂一点高并发多线程都不好意思出去,即使没地方使用,网上大多数相关文档博客也都讲解了这些部分。 我并不想具体介绍什么是volatile,我写这篇博客目的是说明白为什么volatile保证不了线程安全。想要线程安全必须保证原子性,可见性,有序性。而volatile只能保证可见性和有序性 在说明这个问题之前,首先还是要说明下cpu和内存,cpu和内存直接是有高速...

Aaron_涛 1万+

Java并发编程:volatile关键字解析

Java并发编程:volatile关键字解析    volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Java 5之后,volatile关键字才得以重获生机。   volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volati

abckingaa的博客 113

FFmpeg 部署与使用(含Docker部署)

ffmpeg 部署以及使用

weixin_38584583的博客 909

彻底卸载Ubuntu双系统,并完美回收100G空间给D盘

本文记录了安全卸载Ubuntu双系统并回收磁盘空间的全过程。作者首先通过Windows命令行彻底清除Ubuntu启动项,避免GRUB引导报错;随后在磁盘管理中删除Linux分区,将100G未分配空间合并至相邻的D盘。针对C盘空间不足问题,作者采用Dism++清理系统文件,并将用户文档、聊天记录等迁移至扩容后的D盘。最终实现了引导纯净、空间合理分配的目标,特别强调分区扩展必须遵循"相邻原则",建议通过间接迁移而非直接扩容来优化系统盘空间管理。

2403_85993278的博客 1512

浅析volatile原理及其使用

浅析volatile原理及其使用

Innocence_0的博客 213

ThingsBoard入门实战(二):ThingsBoard 设备接入

文章目录ThingsBoard 设备接入一、设备配置/设备类型二、设备三、设备接入HTTPCOAPMQTT四、下一步 ThingsBoard 设备接入 设备接入并不一定需要真正的设备,我们可以通过程序。或者命令来模拟设备,向平台发送信息。 这节我们先为用户分配一个设备,然后通过模拟设备命令更新设备的状态(遥测值)。 一、设备配置/设备类型 添加一个叫"路灯"的设备配置。 设备配置听起来比较抽象,可以认为是设备类型或者是设备类型的配置。实际上,ThingsBoard 中很多时候让选择设备类型,就是选的这个。

AIGC搞起 1万+

java并发编程之——volatile详解

java并发编程之——volatile详解 注:此篇文章转载自http://www.cnblogs.com/dolphin0520/p/3920373.html    volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Java 5之后,volatile关键字才得以重获生机。   vol

u010994169的博客 548

Ansys Zemax|无焦系统设计中的角度单位优化技巧

本文详细介绍了在Ansys Zemax OpticStudio中进行无焦系统设计的核心技巧。重点阐述了从聚焦思维到角度思维的转换,并演示了如何通过开启‘无焦像空间’模式、构建以波前和光束尺寸为目标的评价函数来优化激光扩束镜等系统。文章还分享了使用IMSF操作数处理混合系统以及常见误区避坑的实用指南,旨在帮助光学设计者高效完成无焦系统的设计与分析。

weixin_29231027的博客 357

实测对比:DECIMER、Img2Mol、MolScribe,哪个化学结构识别工具更靠谱?

化学结构识别(CSR)工具的核心任务是将分子结构图像转换为SMILES(Simplified Molecular Input Line Entry System)表示。这一过程需要克服图像质量差异、绘制风格变化以及特殊基团识别等挑战。三款工具采用不同的技术路线:DECIMER:基于EfficientNetV2+Transformer架构,优势在于处理标准期刊文献中的分子图示Img2Mol。

weixin_32576389的博客 341

并发volatile关键字如何保证可见性和有序性及底层实现原理

volatile用法 首先我们先了解一下volatile关键字的用法 ,volatile被喻为轻量级的"synchronized",它只是一个变量修饰符,只能用来修饰变量不能修饰方法和代码块。 经典的用法:双重校验锁实现单例 public class Singleton { private volatile static Singleton singleton; pri...

lc13571525583的博客 1万+

单视图几何Vanish Point(消失点/灭点)计算方法——Robert_T_Collins(罗伯特·柯林斯)算法

单视图几何Vanish Point(消失点/灭点)计算方法——Robert_T_Collins(罗伯特·柯林斯)算法

beyond951的博客 7332

volatile是如何保证可见性和有序性的

volatile保证可见性的原理 可见性问题 可见性问题指的是一个线程在访问一个共享变量的时候,其他线程对该共享变量的修改对于第一个线程来说是不可见的,下面通过一个例子可以发现可见性问题。 public class Visable { private static boolean flag = true; public static void main(String[] args) throws InterruptedException { new Thre.

weixin_37990128的博客 4229

基于表面肌电信号sEMG的手势识别——以Ninapro DB1数据集使用CNN网络识别为例

本文实验采用公共数据集Ninapro。该数据集数据量大,采集的动作丰富,每个单位数据集里面包括表面肌电信号、加速度计、手部运动学和动态数据等,记录了67名完整受试者和11名进行了至少50 次手部运动的截肢者的数据。NinaproDB1数据集包括27名完整的受试者(7 名女性和20名男性,其中2个左手和25 个右手) 的肌电信号。信号采集频率为100Hz。

树下一朵云的博客 2295

YOLOv8改进 | 卷积模块 | 用DWConv卷积替换Conv【轻量化网络】

YOLOv8改进,yolov8,yolov8创新,yolov8涨点

kay_545 7155

volatile如何保证可见性和有序性

volatile原理

bfj11的博客 7200

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

为什么volatile能保证有序性不能保证原子性?

对于内存模型的三大特性:有序性、原子性、可见性。大家都知道volatile能保证可见性和有序性但是不能保证原子性,但是为什么呢?

yh4494的博客 2163

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

Volatile如何保证可见性和有序性?

Volatile如何保证可见性和有序性?

Guyui233的博客 2388

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

为什么volatile保证不了线程安全

为什么Volatile保证不了线程安全

qq_41197578的博客 3140

java中volatile的作用,volatile保证可见性有序性原理,volatile不保证原子性

java中volatile的作用 一、保证可见性 ​ 作用:一个共享变量被volatile修饰时,它会保证将修改后的值立刻更新会主存,当有其他线程需要读取它时,它会去内存读取新值 ​ 普通的共享变量不保证可见性,普通变量被修改后什么时候写回主存是不确定的,当其他线程去读取时,内存中可能还是原来的旧值,因此无法保证可见性 ​ synchronized和Lock也能保证可见性,synchronized和Lock能够保证同一时间内只有一个线程获取锁然后执行同步代码,然后在释放锁后将变量的修改刷新到主存中 ​

weixin_47071411的博客 3201

Juc12_Volatile的可见性、不保证可见性、有序性、使用、内存屏障四大指令StoreStore、StoreLoad 、LoadLoad、LoadStore

Juc12_Volatile的可见性、不保证可见性、有序性、使用、内存屏障四大指令StoreStore、StoreLoad 、LoadLoad、LoadStore

所得皆惊喜 7799

java中volatile关键字保证可见性有序性

我们知道java并发编程主要会有可见性,原子性,有序性几个问题。 volatile可以保证可见性和有序性,但是无法保证原子性。 有序性:编译器会根据自认为的排序对代码进行指令重排,指令重排不会对单线程造成影响,但是可能会导致多线程时候运行错误。 可见性:可行性是指对线程的操作对其他线程可见。 volatile关键字可以禁止指令重排,所以可以保证了有序性。有序性也通常是指的happen-before原则。 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作 锁定规则

xby7437的博客 1万+

7.volatile怎么通过内存屏障保证可见性和有序性?

volatile是怎么保证可见性和有序性的?volatile怎么使用内存屏障?volatile居然都是通过内存屏障保证可见性和有序性!!!

chenzengnian123的博客 3366

原子性、有序性和可见性解释

概念解释: 原子性(Atomicity) 由 Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store 和 write。大致可以认为基本数据类型的操作是原子性的。同时 lock 和 unlock 可以保证更大范围操作的原子性。而 synchronize 同步块操作的原子性是用更高层次的字节码指令 monitorenter 和 monitorexit ...

有的话没说出来之前,你是他的主人,一旦说出来你就成了他的奴隶||想要干好事,别太把自己当人,别把别人太不当人||认识这个人就是开了一扇窗户,就能看到不一样的东西,听到不一样的声音,能让你思考、觉悟,这已经够了 1万+

volatile如何保证可见性

volatile如何保证可见性? 首先要了解一下JMM(java内存模型)原子操作,如下: 1.read(读取):从主内存读取数据 2.load(载入):将主内存的数据写入工作内存 3.use(使用):从工作内存读取数据计算 4.assign(赋值):将计算好的值重新赋值到工作内存中 5.store(存储):将工作内存的值写入主内存 6.write(写入):将store过去的变量值赋值给主内存中的变量 7.lock(锁定):将主内存变量枷锁,标识未线程独占状态 8.unlock(解锁):将主内存变量解锁,解

weixin_38818613的博客 4774
上一篇: redis服务器命令方法详解
下一篇: MESI缓存一致性协议
chastera
博客等级 码龄10年 29粉丝 16原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值