【JUC】二十、volatile变量的特点与使用场景

文章目录

  • 1、volatile可见性案例
  • 2、线程工作内存与主内存之间的原子操作
  • 3、volatile变量不具有原子性案例
  • 4、无原子性的原因分析:i++
  • 5、volatile变量小总结
  • 6、重排序
  • 7、volatile变量禁重排的案例
  • 8、日常使用场景
  • 9、总结

volatile变量的特点:

  • 可见性
  • 禁重排
  • 无原子性

1、volatile可见性案例

volatile的可见性,即保证不同线程对某一个变量一旦完成更改,其他线程立即可见,因为会从线程的工作内存立马刷到主内存。Demo程序:

public class VolatileDemo1 {

    static  boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "come in ...");
            while (flag) {

            }
            System.out.println("flag被设置为false,线程任务执行结束");
        }, "t1").start();
        TimeUnit.SECONDS.sleep(2);
        flag = false;
        System.out.println(Thread.currentThread().getName() + "线程已将flag改为false");
    }
}


flag已被main线程改为false,但t1线程没有收到通知而一直在循环:

在这里插入图片描述

变量改为volatile变量:

static volatile boolean flag = true;

重新运行:

在这里插入图片描述

t1程序可正常停止了,原因就是加了volatile后,flag变量有了可见性,main线程改完后t1可以知道这个变更。

线程t1中为何看不到被主线程main修改为false的flag的值?

可能原因有:

  • 主线程修改了flag之后没有将其刷新到主内存,所以t1线程看不到
  • 主线程将flag刷新到了主内存,但是t1一直自娱自乐,读取的是自己工作内存中fag的值,没有去主内存中更新获取flag最新的值

想解决这个问题需要:

  • 线程中修改了自己工作内存中的副本之后,立即将其刷新到主内存
  • 工作内存中每次读取共享变量时,都去主内存中重新读取,然后拷贝到工作内存。

使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点:

  • 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
  • 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存

2、线程工作内存与主内存之间的原子操作

在这里插入图片描述

  • read:作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存

  • load:作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载

  • use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时就会执行该操作

  • assign:作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时就会执行该操作

  • store:作用于工作内存,将赋值完毕的工作变量的值写回给主内存

  • write:作用于主内存,将store传输过来的变量值赋值给主内存中的变量

  • lock:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程

  • unlock:作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
    在这里插入图片描述

以上面的flag变量为例,说明volatile变量的读写过程:

  • step1:先从主内存中读到,然后load加载到线程t1自己的工作内存(read、load成对出现),开始use到while循环,然后一直循环
  • step2:main线程同样的操作从主内存load到自己的工作内存,但它配合CPU完成了对flag的赋值(assign),并存储(store)到自己的工作内存
  • step3:接下来main线程准备要把这个变更写回主内存了,此时必须加锁lock,写完后解锁unlock
  • step4:上一步的加锁后会清空其他线程工作内存这个变量的值,在使用变量前必须重新load或者assign,因此t1线程可以获取到最新的变量值

3、volatile变量不具有原子性案例

volatile变量的复合操作不具有原子性,比如number++

先看不用volatile的:

class MyNumber{

    int number;

    public synchronized void addPlus(){
        number++;
    }
}

同时开十个线程,每个线程调用1000次addPlus方法:

public class VolatileDemo1 {

    public static void main(String[] args) throws InterruptedException {
        MyNumber myNumber = new MyNumber();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myNumber.addPlus();
                }
            },String.valueOf(i)).start();
        }
        //给上面线程的计算时间
        Thread.sleep(2000);
        System.out.println(myNumber.number);
    }
}

正常输出10000,修改:不用synchronized,变量改为带volatile关键字的:

在这里插入图片描述

运行,接近10000,但不会等于10000:

在这里插入图片描述

4、无原子性的原因分析:i++

在没有加锁的控制时,就没有原子性的保证(synchronized依靠monitor来保证同一时间只能有一个线程来操作):当线程1对主内存对象发起read操作到write操作第一套流程的时间里,线程2随时都有可能对这个主内存对象发起第二套换作,如下图,虚线表示线程2的读取的可能时机:

在这里插入图片描述

从源代码来看,number++只有一行,但对应到底层则是:

在这里插入图片描述

而线程自己的工作内存里,数据加载、计算和赋值这三步,不是原子操作,是可能被分开的。

在这里插入图片描述

对于volatile变量的可见性,JVM只是保证从主内存加载到线程的工作内存的值是最新的,即数据加载这一步是最新的,对比上面的案例:主内存中,volatile修饰的变量number=5,此时线程A和线程B都能读,线程A要进行+1,线程B也要进行+1,但线程B在CPU的调度下一口气走完了数据加载、计算和赋值这三步,并刷回主内存,此时主内存number=6,而线程B比较慢,刚做完+1的计算,但由于volatile的可见性,主内存中已经等于6了,线程B的值作废,去主内存重读,number = 6,然后线程B一路number+1=7并刷到主内存,一看,两个线程,做了三次+1的操作,结果number只是从5变到7,这就是上面结果接近10000但小于10000的原因

在这里插入图片描述

由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须使用加锁同步,加锁,同一时间,最多只能有一个线程进来,每次+1,都能走完写的整体流程,因为其他线程进不来,没有上面那种刚完成数据加载,然后数据就被别的线程改了并刷到主内存,导致自己刚加载的作废的情况。再从i++的字节码来看:

在这里插入图片描述

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。

很明显,i++是个复合操作,分了三步,不具备原子性。如果第二个线程在第一个线程读取旧值和新值写回期间读取了i的值,就会出现两个线程同时对一个值做加1的情况。比如i=6时,线程1完成了6+1,在写回新值前,线程2读取了数据,继续6+1,不管是线程1和线程2谁先写回主内存,(哪怕都写回主内存也是个7,何况volatile下,慢的那一个线程会去主内存重读),都是6+1做了两次,但最后等于7,相当于加了1次

5、volatile变量小总结

volatile变量不适合参与到依赖当前值的运算,比如i = i+1 ; i++之类的

依靠volatile变量的可见性,其适合用于保存某个状态的Boolean值

6、重排序

在这里插入图片描述

重排序是编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,但前提是不能改变原语义,或者说指令不能存在数据依赖关系,数据依赖关系即:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖证。

在这里插入图片描述

若存在数据依赖关系,禁止重排序===> 重排序发生,会导致程序运行结果不同。

在这里插入图片描述

7、volatile变量禁重排的案例

在这里插入图片描述

Demo:

public class VolatileDemo2 {

    int i = 0;
    volatile boolean flag = false;

    public void write(){
        i = 2;
        flag = true;
    }

    public void read(){
        if(flag){
            System.out.println("---i = " + i);
        }
    }
    
}

在每个volatile写操作后面插入写读屏障:
在这里插入图片描述
在每个volatile读操作后面插入读写屏障,

在这里插入图片描述

如此,程序原来的的语义就得到的保证

8、日常使用场景

Case1:单一赋值可以,但是含有复合运算赋值(如i++)不适用

volatile int a = 10;

Case2:在高并发里面,如果是靠变量来通知其他线程来改变后续动作的,那可利用volatile变量的可见性,做状态标志位,判断业务是否结束

volatile boolean flag = false;

做为一个布尔状态标志,判断业务是否该结束了

在这里插入图片描述

Case3:开销较低的读写锁策略

get和increment方法都加synchronized,安全性是保证了,但太重,性能下降太多:

public class Counter{

	private  int vlaue;

	public synchronized int getValue(){  //读也得先拿对象锁
		return value;
	}
	
	public synchronized int incerment(){
		return value++;
	}
}

考虑synchronized结合volatile,此时,也可以每次都读到最新的数据,即使没加锁:

public class Counter{

	private volatile int vlaue;   //volatile

	public int getValue(){  //利用volatile可见性保证并发下也能读取到最新值
		return value;
	}
	
	public synchronized int incerment(){  //利用synchronized保证复合操作的原子性
		return value++;
	}
}

Case4:DCL双端检查锁的禁重排

参考经典文章:这篇循序渐进,都讲明白了:

  • 【单例模式下的DCL】

大概贴下代码:双端检查锁的普通代码:

在这里插入图片描述

问题:

在这里插入图片描述

重排序后,先给变量指向了分配的内存地址,在初始化对象前,多线程下,其他线程获取,判断对象是否为null,很明显,对象内存地址不为null了,但其实对象还没new,后面用它就会空指针:

在这里插入图片描述

在这里插入图片描述

需要给这个变量加volatile关键字来禁止指令重排:

在这里插入图片描述

9、总结

凭什么java写了一个volatile关键字,就可以让系统底层加入内存屏障?两者关系怎么勾搭上的?

在这里插入图片描述

什么是内存屏障?

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

内存屏障能干嘛?
  • 阻止屏障两边的指令重排序
  • 写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
  • 读数据时加入屏障,线程私有工作内存的数据失效,重新到主物理内存中获取最新数据

在这里插入图片描述
在这里插入图片描述

内存屏障的四大指令?
  • 在每一个volatile写操作前面插入一个StoreStore屏障
  • 在每一个volatile写操作后面插入一个StoreLoad屏障
  • 在每一个volatile读操作后面插入一个LoadLoad屏障
  • 在每一个volatile读操作后面插入一个LoadStore屏障

总之:volatile即可见性、禁重排、以及无原子性

  • volatile 写之前的操作,都禁止重排序到 volatile 之后
  • volatile 读之后的操作,都禁止重排序到 volatile 之前
  • volatile 写之后 volatile 读,禁止重排序

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何利用企业软件著作权查询API提升知识产权管理效率

引言 在当今数字化时代&#xff0c;企业的知识产权管理变得愈发重要。其中&#xff0c;软件著作权作为企业重要的知识产权之一&#xff0c;其保护和管理对于企业的创新和竞争力至关重要。为了更高效地进行软件著作权管理&#xff0c;许多企业开始采用先进的技术手段&#xff0…

Chrome清除特定网站的Cookie,从而让网址能正常运行(例如GPT)

Chrome在使用某些网址的时候&#xff0c;例如GPT的时候&#xff0c;可能会出现无法访问这个网址的情况&#xff0c;就是点不动啥的 只需要把你需要重置的网址删除就好了

Leetcode题库(数据库合集)_ 难度:中等

目录 难度&#xff1a;中等1.股票的资本损益2. 当选者3. 页面推荐4. 2016年的投资5. 买下所有产品的人6. 电影评分6. 确认率7. 按分类统计薪水8. 餐馆营业额的变化增长8. 即时食物配送 ①9. 至少有5名直系下属的经理10. 游戏玩法分析11. 好友申请&#xff1a;谁有最多的好友12.…

TypeScript中的类

TypeScript 类 1.TypeScript中类的意义 ​ 相对以前 JavaScript 不得不用 构造函数来充当”类“&#xff0c;TypeScript 类的出现可以说是一次技术革命。让开发出来的项目尤其是大中项目的可读性好&#xff0c;可扩展性好了不是一点半点。 ​ TypeScrip 类的出现完全改变了前…

React创建项目

React创建项目 提前安装好nodejs再进行下面的操作&#xff0c;通过node -v验证是否安装 1.设置源地址 npm config set registry https://registry.npmmirror.com/2.确认源地址 npm config get registry返回如下 https://registry.npmmirror.com/3.输入命令 npx create-re…

ruby安装(vscode、rubymine)

https://rubyinstaller.org/downloads/ 下载exe安装即可 会弹出 输入3 安装成功 vscode插件市场安装ruby插件 新建一个目录&#xff0c;打开terminal bundle init //进行初始化&#xff08;如果执行不了&#xff0c;应该是环境变量没生效&#xff0c;重启vscode&#…

企业架构LB-服务器的负载均衡之LVS实现

企业架构LB-服务器的负载均衡之LVS实现 学习目标和内容 1、能够了解LVS的基本工作方式 2、能够安装配置LVS实现负载均衡 3、能够了解LVS-NAT的配置方式 4、能够了解LVS-DR的配置方式 #一、LVS介绍和安装 LVS&#xff08;Linux Virtual Server&#xff09;即Linux虚拟服务器&…

AWS攻略——VPC初识

大纲 在网络里启动一台可以ssh上去的机器查看区域、VPC和子网创建EC2连接Web端连接客户端连接 知识点参考资料 VPC是在AWS架构服务的基础&#xff0c;有点类似于我们在机房里拉网线和设置路由器等。等这些设施完备后&#xff0c;我们才能考虑给机器部署服务。而很多初识AWS的同…

【OpenGL】窗口的创建

从今天开始我们开始学习OpenGL&#xff0c;从0开始&#xff0c;当然是有C基础的前提 首先包含glad和GLFW的头文件 #include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> 初始化 GLFW 在 main 函数中&#xff0c;我们首先使用 glfwInit 初…

西南科技大学模拟电子技术实验六(BJT电压串联负反馈放大电路)预习报告

一、计算/设计过程 BJT电压串联负反馈放大电路图1-1-1-1为BJT电压串联负反馈放大实验电路,若需稳定输出电压,减小从信号源所取电流,可引入电压串联负反馈闭合开关。 图1-1-1-1 理论算法公式(1)闭环电压放大倍数 (2)反馈系数 (3)输入电阻 (4)输出电阻 计算过程。开环…

Latex公式中矩阵的方括号和圆括号表示方法

一、背景 在使用Latex写论文时&#xff0c;不可避免的涉及到矩阵公式。有的期刊要求矩阵用方括号&#xff0c;有的期刊要求矩阵用圆括号。因此&#xff0c;特记录一下Latex源码在两种表示方法上的区别&#xff0c;以及数组和方程组的扩展。 二、矩阵的方括号表示 首先所有的…

二值图像分割统一项目

1. 项目文件介绍 本章为二值图像的分割任务做统一实现&#xff0c;下面是项目的实现目录 项目和文章绑定了&#xff0c;之前没用过&#xff0c;不知道行不行 data 文件夹下负责摆放数据的训练集测试集inference 负责放待推理的图片(支持多张图片预测分割)run_results 是网络训…

实体、协议、服务和服务访问点

目录 一、概念 二、相邻两层之间的关系 三、面向连接服务的特点 四、无连接服务的特点 五、著名的协议举例 一、概念 实体&#xff08;entity&#xff09;表示任何可发送或接收信息的硬件或软件进程。同机器上同一层的实体叫做对等实体&#xff08;peer entity&#xff0…

【算法专题】前缀和

前缀和 前缀和1. 前缀和【模板】2. 二维前缀和【模板】3. 寻找数组的中心下标4. 除自身以外数组的乘积5. 和为K的子数组6. 和可被K整除的子数组7. 连续数组8. 矩阵区域和 前缀和 1. 前缀和【模板】 题目链接 -> Nowcoder -DP34.前缀和【模板】 Nowcoder -DP34.前缀和【模…

计算机网络:传输层——多路复用与解复用

文章目录 前言一、Socket&#xff08;套接字&#xff09;二、多路复用/解复用三、多路解复用&#xff08;1&#xff09;多路解复用原理&#xff08;2&#xff09;无连接&#xff08;UDP&#xff09;多路解复用&#xff08;3&#xff09;面向连接&#xff08;TCP&#xff09;的多…

15:00的面试,15:06就出来了,问的问题过于变态了。。。

从小厂出来&#xff0c;没想到在另一家公司又寄了。 到这家公司开始上班&#xff0c;加班是每天必不可少的&#xff0c;看在钱给的比较多的份上&#xff0c;就不太计较了。没想到5月一纸通知&#xff0c;所有人不准加班&#xff0c;加班费不仅没有了&#xff0c;薪资还要降40%…

neuq-acm预备队训练week 8 B3647 【模板】Floyd 题解

题目描述 给出一张由 n 个点 m 条边组成的无向图。 求出所有点对(i,j) 之间的最短路径。 题目限制 输入格式 第一行为两个整数 n,m&#xff0c;分别代表点的个数和边的条数。 接下来 m 行&#xff0c;每行三个整数u,v,w&#xff0c;代表 u,v 之间存在一条边权为 w 的边。 …

pip的基本命令和使用

pip 简介 pip是Python官方的包管理器&#xff0c;可以方便地安装、升级和卸载Python包。 pip 常用命令 显示版本和路径 pip --version获取帮助 pip --help升级pip和升级包 pip install --upgrade pip # Linux/macOS pip install -U pip # windowspip install…

基于SSM的图书馆管理系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

剑指 Offer(第2版)面试题 16:数值的整数次方

剑指 Offer&#xff08;第2版&#xff09;面试题 16&#xff1a;数值的整数次方 剑指 Offer&#xff08;第2版&#xff09;面试题 16&#xff1a;数值的整数次方解法1&#xff1a;快速幂 - 递归写法解法2&#xff1a;快速幂 - 非递归写法 剑指 Offer&#xff08;第2版&#xff…