volatile 关键字 (详细解析)

news2025/4/15 18:36:10

目录

前置知识

共享变量不可见性 

JMM

volatile 关键字

使用volatile关键字 

加锁

volatile 关键字 -- 更深入的问题

volatile不保证原子性

volatile禁止指令重排序


前置知识

共享变量不可见性 

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

代码实例 

public class VisibilityDemo01 {
    // main方法,作为一个主线程。
    public static void main(String[] args) {
        // a.开启一个子线程
        MyThread t = new MyThread();
        t.start();

        // b.主线程执行
        while(true){
            if(t.isFlag()){
                System.out.println("主线程进入循环执行~~~~~");
            }
        }
    }
}

class MyThread extends Thread{
    // 成员变量
    private boolean flag = false;
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 触发修改共享成员变量
        flag = true;
        System.out.println("flag="+flag);
    }
    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

我们看到,子线程中已经将flag设置为true,但main()方法中始终没有读到修改后的最新值,从而循环没有能进入到if语句中执行,所以没有任何打印 , 这就是变量的不可见性

JMM

注意区别JMM和JVM

JVM和JMM是有区别的,它们是两个不同的概念:

  1. JVM是Java Virtual Machine(Java虚拟机)的缩写,它是Java编程语言的核心组件之一。JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM负责执行Java程序的指令,并提供一些高级功能,如垃圾回收、内存管理、线程调度等。
  2. JMM是Java Memory Model(Java内存模型)的缩写,它是Java虚拟机规范中定义的一种抽象的概念。JMM定义了线程和主内存之间的抽象关系,即JMM中定义了线程在JVM主内存中的工作方式。JMM规范了Java虚拟机与计算机内存是如何协同工作的,包括如何读取和写入共享变量,以及在必要时如何同步访问共享变量。

JVM负责执行Java程序,并提供高级功能,而JMM则定义了线程和内存之间的抽象关系,以确保Java程序在多线程环境下的正确性 

工作内存 和 主内存概念 

JMM规定如下:

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

现在就可以解释 共享变量不可见性 的原因

  1. 子线程t 和 main方法 从主内存读取到数据放入其对应的工作内存(子线程t 和 main方法 谁先执行不一定),此时 flag 的值为 false
  2. 子线程t 睡眠1秒后,将flag的值更改为true,但是这个时候flag的值还没有写回主内存
  3. 当 子线程flag的值写回去后,但是main方法不会再去读取主存中的值,而是读取自己工作内存中的 flag变量副本,所以while(true)读取到的值一直是false(虽然 main方法 可能会在某一时刻读取主内存中flag 的最新值来刷新flag变量副本,但这个时间我们是无法控制的)

为什么 main方法要去 读取自己工作内存中的 flag变量副本,而不每次都去主内存中读取,这类似 多级缓存的概念,线程从自己的工作内存中读取数据的速度会快于从主内存中读取数据的速度

volatile 关键字

如何实现在多线程下访问共享变量的可见性:也就是实现一个线程修改变量后,对其他线程可见呢?有两种方法

第一种是使用volatile关键字 

第二种是加锁

 

使用volatile关键字 

使用volatile关键字修改该变量

private volatile boolean flag ;

运行结果

我们看到  使用volatile关键字解决了 共享变量不可见性的问题,即 一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。

工作原理 

  1. 子线程t 和 main方法 从主内存读取到数据放入其对应的工作内存,此时 flag的值为false
  2. 子线程t 将flag的值更改为true
  3. 在某一时刻 子线程flag的值写回主内存后,失效其他线程对此变量副本
  4. main方法 再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中

 

加锁

修改main方法

// main方法
while(true) {
    synchronized (t) {
        if(t.isFlag()){
            System.out.println("主线程进入循环执行~~~~~");
        }
    }
}

 运行结果

可以看到同样是解决了 共享变量不可见性的问题

工作原理

  1. 某一个线程进入synchronized代码块前后,执行过程入如下:
  2. 线程获得锁
  3. 清空工作内存
  4. 从主内存拷贝共享变量最新的值到工作内存成为副本
  5. 执行代码
  6. 将修改后的副本的值刷新回主内存中
  7. 线程释放锁

虽然加锁同样能解决 共享变量不可见性的问题,但是 加锁 和 锁的释放 过程都是会有性能消耗的,所以在解决 共享变量不可见性的问题 时,首选 volatile关键字

volatile 关键字 -- 更深入的问题

除了 volatile 可以保证可见性外,volatile 还具备如下一些突出的特性:
  • volatile的原子性问题volatile不能保证原子性操作。
  • 禁止指令重排序:volatile可以防止指令重排序操作。

volatile不保证原子性

原子性:在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行

看如下程序,该程序开启了100个线程,同时对同一个变量进行自增10000次

public class VolatileDemo04 {
    public static void main(String[] args) {
        // 1.创建一个线程任务对象
        Runnable target = new ThreadTarget01();
        // 2.开始100个线程对象执行这个任务。
        for(int i = 1 ; i <= 100 ; i++ ) {
            new Thread(target,"第"+i+"个线程").start();
        }
    }
}

// 线程任务类
class ThreadTarget01 implements Runnable{
    // 定义一个共享变量
    private volatile int count = 0 ;
    @Override
    public void run() {
        synchronized (ThreadTarget01.class){
            for(int i = 1 ; i <= 10000 ; i++ ) {
                count++;
                System.out.println(Thread.currentThread().getName()+"count =========>>>> " + count);
            }
        }
    }
}

最后的结果正常应该是 1000000

但是,实际上是有可能会少于 1000000 的

但是我已经运行了好多次,没有出先少于的情况,所以没运行结果哈哈

原理

count++ 操作包含 3 个步骤:
  • 从主内存中读取数据到工作内存
  • 对工作内存中的数据进行++操作
  • 将工作内存中的数据写回到主内存

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

比如:

  1. 线程A从主存中读取count的值为100,此时由于CPU的切换关系,此时CPU的执行权被切换到了B线程,A线程就处于就绪状态,B线程处于运行状态
  2. 线程B也需要从主内存中读取count变量的值,由于线程A没有对count值做任何修改,因此此时B读取到的数据还是100
  3. 线程B工作内存中count执行了+1操作,但是未刷新到主内存中
  4. 此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主内存,因此A线程工作内存中的变量值还是100,没有失效。
  5. A线程对工作内存中的数据进行了+1操作
  6. 线程B101写入到主内存
  7. 线程A101写入到主内存

虽然计算了2次,但是只对A进行了1次修改 

因此,在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)

要保证原子性操作,有两种方法:1、使用锁机制 2、原子类 这里不在展开讲

volatile禁止指令重排序

重排序:

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

重排序虽然可以提高执行的效率,但是在并发执行下,JVM虚拟机底层并不能保证重排序下带来的安全性等问题,请看如下案例

public class OutOfOrderDemo06 {
    // 新建几个静态变量
    public static int a = 0 , b = 0;
    public static int i = 0 , j = 0;

    public static void main(String[] args) throws Exception {
        int count = 0;
        while(true){
            count++;
            a = 0 ;
            b = 0 ;
            i = 0 ;
            j = 0 ;
            // 定义两个线程。
            // 线程A
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 1;
                    i = b;
                }
            });

            // 线程B
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    j = a;
                }
            });

            t1.start();
            t2.start();
            t1.join(); // 让t1线程优先执行完毕
            t2.join(); // 让t2线程优先执行完毕

            // 得到线程执行完毕以后 变量的结果。
            System.out.println("第"+count+"次输出结果:i = " +i +" , j = "+j);
            if(i == 0 && j == 0){
                break;
            }
        }
    }
}

正常情况下,会有以下三种情况

  • a = 1 ; i=b(0) ; b = 1 ; j = a(1) 最终(i = 0, j = 1)
  • b = 1 ; j=a(0) ; a = 1 ; i = b(1) 最终(i = 1, j = 0)
  • b = 1 ; a=1 ; i = b(1) ; j = a(1) 最终(i = 1, j = 1)

但是,在很小的情况下会出现另外一种结果 i = 0 , j = 0

这就是发生重排序的结果

比如 线程1 中先执行了 i = b,然后切换到 进程2 且先执行 j = a,然后再分别执行 a = 1,b = 1

这样输出的结果就是 i = 0 , j = 0

而使用volatile可以禁止指令重排序,从而修正重排序可能带来的并发安全问题 ,如下

public class OutOfOrderDemo07 {
    // 新建几个静态变量
    public  static int a = 0 , b = 0;
    public volatile static int i = 0 , j = 0;

    public static void main(String[] args) throws Exception {
        int count = 0;
        while(true){
            count++;
            a = 0 ;
            b = 0 ;
            i = 0 ;
            j = 0 ;
            // 定义两个线程。
            // 线程A
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 1;
                    i = b;
                }
            });

            // 线程B
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    j = a;
                }
            });

            t1.start();
            t2.start();
            t1.join(); // 让t1线程优先执行完毕
            t2.join(); // 让t2线程优先执行完毕

            // 得到线程执行完毕以后 变量的结果。
            System.out.println("第"+count+"次输出结果:i = " +i +" , j = "+j);
            if(i == 0 && j == 0){
                break;
            }
        }
    }
}

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

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

相关文章

Android自动化测试之uiautomator2使用

uiautomator2是uiautomator的升级版本&#xff0c;增加了对AccessibilityService服务的支持&#xff0c;当然在appium1.7版本以上进行支持&#xff0c;本篇文章介绍一下它的使用。 安装 安装方式很简单&#xff1a; pip install uiautomator2 进行初始化&#xff1a; pyth…

java版Spring Cloud+Spring Boot+Mybatis+uniapp 企业电子招投标采购系统源码

随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大&#xff0c;公司对内部招采管理的提升提出了更高的要求。在企业里建立一个公平、公开、公正的采购环境&#xff0c;最大限度控制采购成本至关重要。符合国家电子招投标法律法规及相关规范&#xff0c;以及审计监督要…

zabbix 企业级监控(2) 监控linux主机

目录 配置开始 Zabbix添加linux主机 4.为agent.zabbix.com添加模板 环境&#xff1a; &#xff08;隔天做的更换了IP&#xff0c;不影响实际操作&#xff09; IP 192.168.50.50 关闭防火墙规则 更改主机名 [rootlocalhost ~]# vim /etc/hostname agent.zabbix.com [rootloca…

myAgv智能移动底盘的slam算法学习以及动态避障

前言 随着科技得越来越发达&#xff0c;人工智能&#xff0c;自动驾驶导航等字眼频频出现在我们得眼前。但是目前来说自动驾驶并没有得到很全面得普及&#xff0c;还在进行不断的开发和测试当中。从小就爱好车的我&#xff0c;对这项技术也很是感兴趣。 偶然间在上网的时候买…

spring注解开发-spring12

如果使用注解开发&#xff0c;就不需要了写 再测试&#xff0c;也无误 如果你只写类型&#xff0c;只写autowired 如果按照名称注入&#xff0c;你不仅写autowired&#xff0c;还要靠内fea尔 Resource也可以完成注入&#xff1a; 同样是注入bean中id的值 现在我们使用Value…

OpenCV+VS 环境配置(以OpenCV4.7.0+VS2022环境配置为例)

下面以4.7.0版本的OpenCV与VS2022的环境配置为例进行介绍&#xff0c;其他版本的OpenCV与VS的环境配置也可参考本流程。 1.安装OpenCV库 下载网址&#xff1a;https://opencv.org/releases/ 2.配置环境设置 2.1 系统环境变量 1.右击桌面的此电脑图标&#xff0c;点击属性→…

Jsonpath - 数据中快速查找和提取的强大工具

JSON&#xff08;JavaScript Object Notation&#xff09;在现代应用程序中广泛使用&#xff0c;但是如何在复杂的JSON数据中 查找和提取所需的信息呢&#xff1f;JSONPath是一种功能强大的查询语言&#xff0c;可以通过简单的表达式来快速准确地定位和提取JSON数据。本文将介绍…

HarmonyOS/OpenHarmony应用开发-Stage模型UIAbility组件使用(五)

UIAbility组件间交互&#xff08;设备内&#xff09; UIAbility是系统调度的最小单元。在设备内的功能模块之间跳转时&#xff0c;会涉及到启动特定的UIAbility&#xff0c;该UIAbility可以是应用内的其他UIAbility&#xff0c;也可以是其他应用的UIAbility&#xff08;例如启动…

飞行动力学 - 第11节-纵向静稳定性及各部件贡献 之 基础点摘要

飞行动力学 - 第11节-纵向静稳定性及各部件贡献 之 基础点摘要 1. 气流角2. 操纵面偏角3. 系数的符号4. 纵向、横向、航向稳定性5. 纵向静稳定性5.1 定义5.2 准则5.3 举例5.4 假设5.5 分析5.5.1 机身贡献5.5.2 机翼贡献5.5.3 尾翼贡献 6. 参考资料 1. 气流角 迎角&#xff1a;…

第二章 表的操作与数据类型

第二章 表的操作 一、表的创建&#xff08;1&#xff09;语法&#xff08;2&#xff09;示例 二、查看库中所有表以及具体表结构&#xff08;1&#xff09;语法&#xff08;2&#xff09;示例 三、表的修改&#xff08;1&#xff09;语法&#xff08;2&#xff09;示例 四、表的…

cesium实战(1)、cesium 加载本地json、GeoJson数据

1、cesium加载本地图层json图层数据 并设置样式 添加图层 // 加载路网数据 wms数据服务let addRoadLayer () > {Cesium.GeoJsonDataSource.load(/cesium/layers/road_84.json, {stroke: Cesium.Color.YELLOW,//多边形或线的颜色 strokeWidth: 3,//多边形或线 宽度clampToG…

win10/win11 无线显示器 启用输入 的问题分析与解决

win10、win11系统自带了“无线显示器”应用&#xff0c;可以作为接收端接受其他PC或者手机等设备的投屏显示。 但是使用手机等设备投屏&#xff0c;尤其是三星、华为等手机的类PC模式时总会提示“要启用输入&#xff0c;请转到你的电脑&#xff0c;选择“操作中心”>“连接…

连接另一台电脑的虚拟机

在一个局域网中&#xff0c;ping通另一台电脑是件很容易的事。但是经常会遇到&#xff0c;需要ping到另一台电脑里面的虚拟机&#xff0c;因为我们需要连接它的数据库&#xff0c;或者其他服务。 假设PC A要连接PC B上的虚拟机C。 我们需要做的是&#xff1a; 将C与B的网络连…

C语言数据结构(链表概念讲解和插入操作)

文章目录 前言一、什么是链表二、链表的优点和缺点三、链表节点的定义四、初始化链表五、链表的插入1.头部插入2.尾部插入3.中间插入 六、遍历链表七、释放链表总结 前言 本篇文章带大家正式的来学习数据结构&#xff0c;数据结构是学习操作系统&#xff0c;和深入C语言必不可…

影响伦敦金走势的两大因素是什么?

在伦敦金市场中经历过一段时间&#xff0c;很多人发现&#xff0c;其实要精准预测伦敦金的走势&#xff0c;尤其是短线的走势&#xff0c;是非常难的。但是&#xff0c;判断其大势&#xff0c;却是有一定规律的。在投资世界中&#xff0c;伦敦金投资之外的一些品种的涨跌号称是…

Ubuntu22.04安装显卡驱动(高速、避错版)

关于显卡驱动安装踩坑不少坑&#xff0c;前前后后重装了6、7次&#xff0c;总结了一下目前网上的各种安装方式&#xff0c;整理了本文。 目录导航 1 准备工作1.1 关闭安全模式1.2 切换独显模式1.3 更新软件列表和安装必要软件、依赖1.4 禁用nouveau (nouveau是通用的驱动程序)1…

SpringCloud学习路线(5)—— Nacos配置管理

一、统一配置管理 需求&#xff1a; 微服务配置能实现统一的管理&#xff0c;比如希望改动多个配置&#xff0c;但不希望逐个配置&#xff0c;而是在一个位置中改动&#xff0c;并且服务不用重启即用&#xff08;热更新&#xff09;。 &#xff08;一&#xff09;使用配置管理…

【大模型】与 ChatGPT 齐平、可商用、更强的 LLaMA2 来了

【大模型】可商用且更强的 LLaMA2 来了 LLaMA2 简介论文GitHubhuggingface模型列表训练数据训练信息模型信息 许可证参考 LLaMA2 简介 2023年7月19日&#xff1a;Meta 发布开源可商用模型 Llama 2。 Llama 2是一个预训练和微调的生成文本模型的集合&#xff0c;其规模从70亿到…

【极简 亲测】已拦截跨源请求:同源策略禁止读取位于....的远程资源。(原因:CORS 头缺少 ‘Access-Control-Allow-Origin‘)

CORS是Cross-Origin Resource Sharing。 解决 首先这个是浏览器层面的拦截。下面的方法都是解除浏览器拦截的方式。 解除了之后还是有可能其他方面有问题的&#xff0c;但是那个会提示其他错误。 比如CORs Failed之类的&#xff0c;这个是没收到response&#xff0c;大概率是…

施耐德plc编程软件转以太网模块

捷米特JM-ETH-SC 是一款经济型的以太网通讯处理器&#xff0c;是为满足日益增多的工厂设备信息化需求&#xff08;设备网络监控和生产管理&#xff09;而设计&#xff0c;用于施耐德Quantumn/Premiun/TSXMicro/Twdio/M200/M218/M221/M241/M238/M25 等系列 PLC 的以太网数据采集…