volatile 关键字

一篇文章帮你搞懂volatile JMM JMM(Java内存模型,Java Memory Model)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过规范定制了程序中的各个变量的访问方式。 JMM关于同步的规定: 线程解锁前,必须把共享变量的值刷新回主内存。 线程加锁前,必须读取主内存的最新值到自己的工作内存。 加锁解锁必须是同一个锁。 特点: 原子性:即一个操作或多个操作在执行的过程中,要成功都成功,要失败都失败。 可见性:多个线程访问同一个变量时,当一个线程修改该变量时,其他线程可见。 有序性:保证程 阅读详情

1052663546-1-30080

提示:volatile 表示 “不稳定” 的意思。用于修饰共享可变变量,即没有使用 final(不可变变量) 关键字修饰的实例变量或静态变量,相应的变量就被称为 volatile 变量,如下:

volatile 表示 “不稳定” 的意思。用于修饰共享可变变量,即没有使用 final(不可变变量) 关键字修饰的实例变量或静态变量,相应的变量就被称为 volatile 变量,如下:

private volatile String name;

一、volatile 的作用

volatile 关键字的作用包括:保障可见性、有序性和 long/double 型变量读写操作的原子性。一般而言,对 volatile 变量的赋值操作,其右边表达式中只要涉及共享变量(包括被赋值的 volatile 变量本身),那么这个赋值操作就不是原子操作。要保障这样操作的原子我们仍需要借助锁。

volatile Map map = new HashMap(); 可以分解为如下伪代码所示的几个子操作:
【1】objRef = allocate(HashMap.class); //子操作①:分配对象所需的存储空间。
【2】invokeConstructor(objRef); //子操作②:初始化 objRef 引用的对象。
【3】map = objRef; //子操作③:将对象引用写入变量 map。

虽然 volatile 关键字仅保障其中的子操作③是一个原子操作,但是由于子操作①和子操作②仅涉及局部变量而未涉及共享变量,因此对变量 map 的赋值操作仍然是一个原子操作。

写线程对 volatile 变量的写操作会产生类似于释放锁的效果。读线程对 volatile 变量的读操作会产生类型于获得锁的效果。因此,volatile 具有保障有序性和可见性的作用。

对于 volatile 变量的写操作,Java 虚拟机会在该操作之前插入一个释放屏障[禁止 volatile 写操作与该操作之前的任何读、写操作进行重排序,从而保证了 volatile 写操作之前的任何读、写操作会先于 volatile 写操作被提交],并在该操作之后插入一个存储屏障[具有冲刷处理器缓存的作用,因此在 volatile 变量写操作之后插入的一个存储屏障就使得该存储屏障前所有操作的结果(包括 volatile 变量写操作及该操作之前的任何操作)对其他处理器来说是可同步的],如下图:

对于 volatile 变量读操作,Java 虚拟机会在该操作之前插入一个加载屏障(Load Barrier),并在该操作之后插入一个获取屏障(Acquire Barrier),如下图:

加载屏障通过冲刷处理器缓存,使其执行线程(读线程)所在的处理器将其他处理器对共享变量(可能是多个变量)所做的更新同步到该处理器的高速缓存中。读线程执行的加载屏障和写线程执行的存储屏障配合在一起使得写线程对 volatile 变量的写操作以及在此之前所执行的其他内存操作的结果对读线程可见,即保障了可见性。因此 volatile 不仅仅保障了 volatile 变量本身的可见性,还保障了写线程在更新 volatile 变量之前执行的所有操作的结果对读线程可见。这种可见性保障类似于锁对可见性的保障,与锁不同的是 volatile 不具备排他性,因而它不能保障读线程读取到的这些共享变量的值是最新的,即读线程读取到这些共享变量的那一刻可能已经有其他写线程更新了这些共享变量的值。另外,获取屏障禁止了 volatile 读操作之后的任何操作读,写操作与volatile 读操作进行重排序。因此它保障了 volatile 读操作之后的任何操作开始执行之前,写操作对相关共享变量(包括 volatile 变量和普通变量)的更新已经对当前线程可见。

【volatile 在有序性保障方面也可以从禁止重排序的角度理解,即 volatile 禁止了如下重排序】:
【1】写 volatile 变量操作与该操作之前的任何读、写操作不会被重排序;
【2】读 volatile 变量操作与该操作之后的任何读、写操作不会被重排序;

二、volatile 变量的开销

        volatile 变量的开销包括读变量和写变量两个方面。volatile 变量的读、写操作都不会导致上下文切换,因此 volatile 的开销比锁要小。写一个 volatile 变量会使该操作以及该操作之前的任何写操作的结果对其他处理器是可同步的,因此 volatile 变量写操作的成本介于普通变量的写操作和在临界区进行的写操作之间。读取 volatile 变量的成本也比在临界区中读取变量要低(没有锁的申请与释放以及上下文切换的开销),但是其成本可能比读取普通变量要高一些。这是因为 volatile 变量的值每次都需要从高速缓存或者主内存中读取,而无法被暂存在寄存器中,从而无法发挥访问的高效性。

三、volatile 的典型应用场景

【场景一】:使用 volatile 变量作为状态标志。在该场景中,应用程序的某个状态由一个线程设置,其他线程会读取该状态并以该状态作为其计算的依据(或者仅仅读取并输出这个状态值)。此时使用 volatile 变量作为同步机制的好处是一个线程能够“通知”另外一个线程某种事件(例如,网络连接断连之后重新连上)的发生,而这些线程又无须使用锁,从而避免了锁的开销以及相关问题。
【场景二】:使用 volatile 保障可见性。 在该场景中,多个线程共享一个可变状态变量,其中一个线程更新了该变量之后,其他线程在无须加锁的情况下也能够看到该更新。
【场景三】:使用 volatile 变量代替锁。 volatile 关键字并非锁的代替品,但是在一定的条件下它比锁更合适(性能开销小,代码简单)。多个线程共享一组可变状态变量的时候,通常我们需要使用锁来保障对这些变量的更新操作的原子性,以避免产生不一致问题。利用 volatile 变量写操作具有的原子性,我们可以把这一组可变状态变量封装成一个对象,那么对这些状态变量的更新操作就可以通过创建一个新的对象并将该对象引用赋值给相应的引用型变量来实现。在这个过程中,volatile 保障了原子性和可见性,从而避免了锁的使用。

volatile 关键字并非锁的替代品,volatile 关键字和锁各有其使用条件。前者更适合与多个线程共享一个状态变量(对象),而后者更适合于多个线程共享一组状态变量。某些情形下,我们可以将多个线程共享的一组状态变量合并成一个对象,用一个 volatile 变量来引用该对象,从而使我们不必要使用锁。

【场景四】:使用 volatile 实现简易版读写锁。 

在该场景中,读写锁是通过混合使用锁和volatile变量而实现的,其中锁用于保障共享变量写操作的原子性,volatile变量用于保障共享变量的可见性。因此,与ReentrantReadWriteLock所实现的读写锁不同的是,这种简易版读写锁仅涉及一个共享变量并且允许一个线程读取这个共享变量时其他线程可以更新该变量(这是因为读线程并没有加锁)。因此,这种读写锁允许读线程可以读取到共享变量的非最新值。该场景的一个典型例子是实现一个计数器:

public class Counter {
    private volatile long count;
    public long value(){
        return count;
    }

    public void increment() {
        synchronize(this){
            count++;
        }
    }
}

四、volatile 实现可见性的原理

在 Java 并发编程中,一定绕不开 volatile、synchronize、lock 几个关键字,其中 volatile 关键字是用来解决共享变量(类成员变量、类的静态成员变量等)的可见性问题,非共享成员变量(局部变量)是分配在 JVM 虚拟机的栈中,是线程私有的,不涉及可见性问题。

【可见性】: 在 JAVA规范中是这样定义的:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。通俗的讲就是如果有一个共享变量N,当有两个线程T1、T2同时获取了N的值,T1修改N的值,而T2读取N的值。那么可见性规范要求T2读取到的必须是T1修改后的值,而不能在T2读取旧值后T1修改为新值。volatile关键字修饰的共享变量可以提供这种可见性规范,也叫做读写可见。那么底层实现是通过机制保证 volatile变量读写可见的?Volatile的实现机制: 在说这个问题之前,我们先看看CPU是如何执行 Java 代码的。

首先 Java代码会被编译成字节码 .class文件,在运行时会被加载到 JVM中,JVM会将 .class转换为具体的 CPU执行指令,CPU加载这些指令逐条执行。

以多核CPU为例(两核),我们知道 CPU的速度比内存要快得多,为了弥补这个性能差异,CPU 内核都会有自己的高速缓存区,当内核运行的线程执行一段代码时,首先将这段代码的指令集进行缓存行填充到高速缓存,如果非 volatile 变量当 CPU执行修改了此变量之后,会将修改后的值回写到高速缓存,然后再刷新到内存中。如果在刷新回内存之前,由于是共享变量,那么CORE2 中的线程执行的代码也用到了这个变量,这是变量的值依然是旧的。volatile关键字就会解决这个问题的,如何解决呢,首先被 volatile关键字修饰的共享变量在转换成汇编语言时,会加上一个以 lock为前缀的指令,当CPU发现这个指令时,立即做两件事:
【1】将当前内核高速缓存行的数据立刻回写到内存;
【2】使在其他内核里缓存了该内存地址的数据无效;

第一步很好理解,第二步如何做到呢?

 【MESI协议】: 在早期的 CPU中,是通过在总线加 LOCK#锁的方式实现的,但这种方式开销太大,所以 Intel开发了缓存一致性协议,也就是 MESI协议[伊利诺斯],该解决缓存一致性的思路是:当 CPU写数据时,如果发现操作的变量是共享变量,即在其它 CPU中也存在该变量的副本,那么它会发出信号通知其他 CPU将该变量的缓存行设置为无效状态。当其它 CPU使用这个变量时,首先会去嗅探是否有对该变量更改的信号,当发现这个变量的缓存行已经无效时,会从新从内存中读取这个变量。以上这些就是 volatile关键字的内部实现机制。

软件工程大作业——图书管理系统/图书个性化推荐与实现系统 软件工程期末大作业,图书管理系统,包含ER图、程序流程图、属性图、结构图 阅读详情

相关推荐

基于deepseek+dify+达梦实现自然语言查询数据库

自然语言查询数据库信息

weixin_43974943的博客 1592

Quantum Espresso Hands-On实战训练(四)——PDOS计算与轨道分析

本文详细介绍了Quantum Espresso中PDOS计算与轨道分析的实战方法,重点解析了PDOS在材料电子结构研究中的核心价值,包括原子级分辨率、轨道级解析和自旋极化分析。通过具体案例和操作步骤,帮助读者掌握从输入文件准备到结果解读的全流程,并提供了高级分析技巧和疑难问题解决方案。

weixin_29211309的博客 504

深入解析poi-tl自定义表格渲染插件中的多级子循环实现

本文深入解析了poi-tl自定义表格渲染插件中的多级子循环实现,详细介绍了其在处理复杂业务数据时的核心价值、实现原理及实战技巧。通过多级子循环机制,开发者可以高效地将层级数据结构渲染到Word表格中,显著提升代码简洁性和维护效率。文章还分享了性能优化和异常处理经验,帮助开发者更好地应用该插件。

weixin_33734785的博客 483

C++坦克大战源代码

【代码】C++坦克大战源代码。

强国豪,林中霖 2608

通信原理 | python调制识别数据及代码

数据集包含了从-20dB 到+18dB 总共 20 个信噪比(步长为 2)下的 11 种调制信号, 包括 AM-DSB、 AM-SSB 和 WBFM 三种模拟调制信号,以及 BPSK、 QPSK、 8PSK、 CPFSK、 GFSK、 PAM4、 QAM16 和 QAM64 八种数字调制信号。其中信号的中心频率为 200KHz,采样频率为 1Msamp/s,且每个信噪比下每种调制信号包含 1000 个信号。其中每个信号包含 IQ 两路数据,且每一路数据都包含有 128 个采样点。

若北辰 1758

AUTOSAR知识点 之 XCP (二):XCP基础概念

学习XCP的过程中,对XCP的基本概念要做一个全面的认知,因为里面有很多缩写以及难以理解的点,假设对此模模糊糊一知半解,那么对整个协议栈的认知会遇到极大的障碍。XCP的信号流传输图示如下XCP 标准主要分为 5 个部分:这部分提供了对于协议的一个概况介绍,包括 XCP 的功能和基础性内容。这部分内容对协议层进行了详细的规范和说明。

梦想技术家 2769

YOLOv11改进 | Neck篇 | 独创HFPN利用分层特征融合块HFFB模块融合多层次特征改进yolov11(全网独家创新)

本文给大家带来的最新改进是利用分层特征融合块HFFB创新yolov11的neck部分我称之为HFPN,这个模块可以融合局部特征、全局特征、中间特征将三种特征融合在一起辅助yolov11进行检测,经过我的设计分为三种可以针对大目标、小目标、标准目标的检测方式均不同,大家可以根据自己的数据集进行不同的选择,本文的内容为我独家创新。YOLOv11改进系列专栏——本专栏持续复习各种顶会内容——科研必备目录一、本文介绍二、原理介绍三、核心代码四、添加方法4.1 修改一4.2 修改二4.3 修改三4.4 修改四。

Snu77的博客 5205

ADRC自抗扰算法优化(PLC梯形图篇)

ADRC自抗扰算法PLC梯形图完整源代码请参看下面博客文章:ADRC自抗扰控制算法(含梯形图完整源代码和算法公式)_adrc算法_RXXW_Dor的博客-优快云博客PLC的自抗扰控制(ADRC)算法_RXXW_Dor的博客-优快云博客_adrc算法1、自抗扰控制算法,网上很多文章有所讲解,大家也可以关注韩京清教授的论文和书籍,这里就不赘述了,下面给出梯形图+SCL代码。

RXXW_Dor的博客 2282

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

Camera2Demo

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

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

上一篇: CPU、内存过高问题定位
下一篇: 搞钱实用指南
清水乐园
博客等级 码龄13年 175粉丝 398原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清水乐园

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值