深入理解volatile关键字

一、简介

volatile是 Java提供的一种轻量级的同步机制。Java包含两种内在的同步机制:同步块(或方法)和 volatile 变量相比于synchronized (synchronized常称为重量级)volatile是更轻量级的,因为它不会引起线程上下文的切换和调度。但是volatile变量的同步性较差,而且其使用也更容易出错。

二、可见性问题

1. 可见性案列

public class TestVolatile {
    private  static boolean flag = false;
    //private volatile static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            flag = true;
            System.out.println("=======循环之前=======");
            while (flag) {

            }
            System.out.println("=======循环之后=======");
        });
        thread1.start();

        Thread.sleep(200);

        Thread thread2 = new Thread(() -> {
            System.out.println("修改flag之前...");
            System.out.println(flag); // true

            flag = false;
            System.out.println("修改flag之后...");
            System.out.println(flag);  // false 上面的线程没有跳出循环
        });
        thread2.start();
    }
}

这段代码的作用就是当启用thread1时进入一个线程,然后休眠一段时间确保thread2 的运行在thread1之后。然后启用thread2让thread2修改flag值让程序退出循环。但这个程序会按照我们想要的方式运行吗?
在这里插入图片描述

看程序运行的结果可以发现程序并没有退出循环,也就是说thread2修改了值,thread1并不知道,所以在thread1中flag的值还是为true。为什么会出现这样的情况呢?这就要谈到我们的JMM内存模型了。

2. JMM内存模型

JMM 决定一个线程对共享变量的写入何时对另一个线程可见, J M M 定义了线程和主内存之间的抽象关系:共享变量存储在主内存( M ain Memory) 中,每个线程都有一个私有的本地内存 (Local Memory) ,本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都须在工作内存中进行,而不能直接读写主内存中的变量。

在这里插入图片描述

从图中可以知道我们每个线程在工作时用到的都是工作内存,当thread2修改flag值时,并没有把修改的值同步到主内存中,而thread也无法从主内存中读取到thread2的修改值,所以thread1他并不知道flag的值被修改成了false。那要怎么解决这个问题呢?解决这种共享变量在多线程椟型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用 synchronized 或者 Lock 这些方式太重量级了,比较合理的方式其实就是vo|atile。那vo|atile是怎么解决可见性问题呢?

3. 解决办法

public class TestVolatile {
//    private  static boolean flag = false;
    private volatile static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            flag = true;
            System.out.println("=======循环之前=======");
            while (flag) {

            }
            System.out.println("=======循环之后=======");
        });
        thread1.start();

        Thread.sleep(200);

        Thread thread2 = new Thread(() -> {
            System.out.println("修改flag之前...");
            System.out.println(flag); // true

            flag = false;
            System.out.println("修改flag之后...");
            System.out.println(flag);  // false 上面的线程没有跳出循环
        });
        thread2.start();
    }
}

在这里插入图片描述

当我们把变量使用 volatile 修饰时 private volatile static boolean flag = false;,thread2对变量进行操作时,会把变量变化的值强制刷新的到主内存。当thread1获取值时,会把自己的内存里的 flag值过期掉,之后从主内存中读取。所以添加关键字后程序如预期输出结果。

在这里插入图片描述

从运行的结果上来看我们的程序已经退出了循环volatile的确是解决了可见性问题。那除了可见性问题,voliate还可以解决什么样的问题呢?

三、指令重排问题

Java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。指令重排序的意义是什么?JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。从 Java 源代码到最终执行的指令序列,会分别经历下面3种重排序:

在这里插入图片描述

1. 指令重排案列

public class Singleton {
    private static Singleton singleton;
    public static Singleton getSingleton(){
        if (Objects.isNull(singleton)){
            //有可能很多线程阻塞到拿锁,拿完锁再判断一次
            synchronized (Singleton.class){
                if (Objects.isNull(singleton)){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

以上代码是一个单列模式,但是这个单列模式有一个大问题,那就是 singleton = new Singleton()这行代码会出现指令重排。我么可以理解为了以下3行代码。

a. memory = allocate() //分配内存
b. ctorInstanc(memory) //初始化对象
c. singleton = memory   //设置instance指向刚分配的地址

假设A、B两个线程同时执行代码。当A线程执行到singleton = new Singleton()这行代码时,发生了指令排序A线程执行了a、c代码没有执行b代码,此时B线程正好执行到 if (Objects.isNull(singleton))这个判断语句。因为A线程执行了c代码所以singleton不等于空,因此B线程就会返回一个没有初始化的singleton对象。

2. 解决问题

解决指令重排我们也可以在if (Objects.isNull(singleton))前面再加一个锁,但是这样的解决办法也太重量级了。因此我们也可以使用volatile来解决指令重排问题。那volatile是如何解决指令重排问题?

public class Singleton {
    private volatile static Singleton singleton;
    public static Singleton getSingleton(){
        if (Objects.isNull(singleton)){
            //有可能很多线程阻塞到拿锁,拿完锁再判断一次
            synchronized (Singleton.class){
                if (Objects.isNull(singleton)){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

3.内存屏障

内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。JMM提供了4种内存屏障。

屏障类型指令示例说明
LoadLoadLoad1; LoadLoad; Load2保证load1的读取操作在load2及后续读取操作之前执行
StoreStoreStore1; StoreStore; Store2在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存
LoadStoreLoad1; LoadStore; Store2在stroe2及其后的写操作执行前,保证load1的读操作已读取结束
StoreLoadStore1; StoreLoad; Load2保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略

  • 在每个volatile写操作的前面插入一个StoreStore屏障。禁止上面的普通写和下面的volatile写重排序。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。防止上面的volatile写与下面可能有的volatile读/写重排序。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。禁止上面的volatile读和下面所有的普通读操作重排序。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。禁止上面的volatile读和下面所有的普通写操作重排序。

正是有了内存屏障的存在,才能让volatile能够禁止指令重排的问题。

四、总结

  • volatile会控制被修饰的变量在内存操作上主动把值刷新到主内存,JMM 会把该线程对应的CPU内存设置过期,从主内存中读取最新值。
  • volatile 的内存屏故障是在读写操作的前后各添加一个 StoreStore屏障,也就是四个位置,来保证重排序时不能把内存屏障后面的指令重排序到内存屏障之前的位置。
  • volatile 并不能解决原子性,如果需要解决原子性问题,需要使用 synchronzied 或者 lock。

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

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

相关文章

LeetCode25: K 个一组翻转链表

题目描述 给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。 k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。 你不能只是单纯的改变节点…

B树系列(详解)

目录 一、B-树 二、B树 三、B*树 四、时间复杂度 五、Mysql与B树系列 一、B-树 首先再说B树的性质以及其他的之前,先要说一声,好多人都把这个树叫B减树,其实不是,他就叫B树,至于原因我觉的没必要再这个名字上纠结…

HTTPS证书怎么申请?多少钱?

HTTPS证书的申请费用会因多种因素而异,主要包括以下几点: 1. 证书类型: - 域名验证型证书(DV SSL):这种证书仅验证域名的所有权,申请速度较快,通常用于个人网站或小型项目,价格相对较低&…

Java对象大小计算

概述 在实际应用中,尤其是在进行JVM调优时,理解并正确估计对象大小是非常重要的,因为这直接影响到内存分配、垃圾回收效率以及应用程序的整体性能。 对象的组成 在Java中,计算一个对象的大小是为了了解它在内存中占用的确切空间…

vue2后台管理系统demo,包含增删查改、模糊搜索、分页

因一直敲小程序,vue不熟练,自己练手项目,就包含增删查改以及模糊搜索分页 一、页面简单但功能齐全 二、数据是mock模拟 三、启动步骤 1、 json-server --watch data.json 启动mock数据 2、npm i 下载依赖 3、npm run serve 四、github地址…

【机器学习:推荐系统】什么是推荐系统?

【机器学习:推荐系统】什么是推荐系统? 推荐系统如何工作?推荐系统生命周期推荐系统算法使用推荐系统的好处推荐系统的类型协同过滤基于内容的过滤混合过滤 现实生活中的推荐系统示例亚马逊Spotify脸书/元数据Netflix谷歌和 YouTube 关于推荐…

BUUCTF---另外一个世界1

1.这是一道杂项题,也是我觉得最值得记录的一道题。 2.话不多说,题目描述(真的是另一个世界) 3.下载附件,是一张图片 4.尝试了查看属性,以及在记事本中打开看看有没有什么有用的信息,发现没什么…

基于单片机的节能窗控制系统设计

摘 要:本文以单片机为基础,对节能窗控制系统进行了科学设计,在满足日常生活需求的同时更好地实现节能减排目标。此设计中的节能窗控制系统,实际操作要灵活,具备可靠且稳定的性能,同时具备节能功效。 关键词:单片机;节能窗控制系统;系统设计 在节能窗等概念推广的背景…

Java 下载excel文件

一、背景 微信小程序需要导出excel文件,后端技术Java,前端使用uniapp框架,使用excel模板。 二、excel 报表模板 需要补充的内容是以下标记问号的,其中有个表格,内容是动态添加的 三、Java端代码实现 关键步骤&…

LabVIEW流量控制系统

LabVIEW流量控制系统 为响应水下航行体操纵舵翼环量控制技术的试验研究需求,通过LabVIEW开发了一套小量程流量控制系统。该系统能够满足特定流量控制范围及精度要求,展现了其在实验研究中的经济性、可靠性和实用性,具有良好的推广价值。 项…

【OpenCV】绘制桌面锁屏时钟

OpenCV 是一个开源的计算机视觉(Computer Vision)与机器学习软件库,提供了多种图像处理算法与接口,用于解决计算机视觉相关问题。OpenCV 支持多平台与多语言,本文主要记录如何使用 OpenCV-Python 绘制桌面锁屏时钟。 目…

NX二次开发:ListingWindow窗口的应用

一、概述 在NX二次开发的学习中,浏览博客时发现看到[社恐猫]和[王牌飞行员_里海]这两篇博客中写道有关信息窗口内容的打印和将窗口内容保存为txt,个人人为在二次开发项目很有必要,因此做以下记录。 ListingWindow信息窗口发送信息四种位置类型 设置Listi…

【VPX637】基于XCKU115 FPGA+ZU15EG MPSOC的6U VPX双FMC接口通用信号处理平台

VPX637是一款基于6U VPX总线架构的通用实时信号处理平台,该平台采用一片Xilinx的高性能Kintex UltraScale系列FPGA(XCKU115-2FLVF1924I)作为预处理单元,外挂2个FMC扩展接口,来完成数据采集、数据回放以及实时信号处理算…

NLP Seq2Seq模型

🍨 本文为[🔗365天深度学习训练营学习记录博客🍦 参考文章:365天深度学习训练营🍖 原作者:[K同学啊 | 接辅导、项目定制]\n🚀 文章来源:[K同学的学习圈子](https://www.yuque.com/mi…

wireshark抓取localhost(127.0.0.1)数据包

打开wireshark中,在"capture"菜单中,选择"interfaces"子菜单,在列出的接口中选中"Adapter for loopback traffic capture"即可。 必须安装了Npcap才有此选项,否则需要重新安装wireshark。 抓包截图…

Vue+SpringBoot打造城市桥梁道路管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统展示四、核心代码4.1 查询城市桥梁4.2 新增城市桥梁4.3 编辑城市桥梁4.4 删除城市桥梁4.5 查询单个城市桥梁 五、免责说明 一、摘要 1.1 项目介绍 基于VueSpringBootMySQL的城市桥梁道路管理系统,支持…

gpt批量工具,gpt批量生成文章工具

GPT批量工具在今天的数字化时代扮演着越来越重要的角色,它们通过人工智能技术,可以自动批量生成各种类型的文章,为用户提供了便利和效率。本文将介绍5款不同的GPT批量工具,并介绍一款知名的147GPT生成工具,以及另外一款…

beets,一个有趣的 Python 音乐信息管理工具!

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站AI学习网站。 目录 前言 什么是Beet库? 安装Beet库 使用Beet库 Beet库的功能特性 1. 多种音乐格式支持 2. 自动标签识…

ECMAScript-262 @2023版本中的关键字和保留字

1、什么是标识符? 所谓标识符,就是javascript里的变量、函数、属性或函数参数的名称,可由一个或多个字符组成,当然标识符有命名规范 标识符第一个字符必须是 一个字母、下划线(_)或美元符号($…

在 Rust 中实现 TCP : 1. 联通内核与用户空间的桥梁

内核-用户空间鸿沟 构建自己的 TCP栈是一项极具挑战的任务。通常,当用户空间应用程序需要互联网连接时,它们会调用操作系统内核提供的高级 API。这些 API 帮助应用程序 连接网络创建、发送和接收数据,从而消除了直接处理原始数据包的复杂性。…