Java 多线程之 volatile(可见性/重排序)

文章目录

    • 一、概述
    • 二、使用方法
    • 三、测试程序
      • 3.1 验证可见性的示例
      • 3.2 验证指令重排序的示例

一、概述

  • 在Java中,volatile 关键字用于修饰变量,其作用是确保多个线程之间对该变量的可见性禁止指令重排序优化

  • 当一个变量被声明为volatile时,线程在读取和写入该变量时会直接操作主内存中的值,而不会使用线程自己的工作内存。这意味着当一个线程修改了一个volatile变量的值时,其他线程将立即看到这个变化,而不会使用缓存中的旧值。底层原理应该是实现了多CPU缓存一致性协议(如MESI),保证了线程可见性。如下图

    在这里插入图片描述

  • volatile关键字还可以防止指令重排序优化。在多线程环境下,为了提高执行效率,编译器和处理器可能会对指令进行重排序。然而,这种重排序可能会导致多线程程序出现意想不到的结果。通过使用volatile关键字,可以确保特定操作的执行顺序与程序中的顺序一致,从而避免了指令重排序可能引发的问题。底层原理应该是使用了屏障(如loadfence、storefence原语指令),禁止指令重排序。

  • volatile 关键字只能保证单个变量的原子性操作和可见性,并不能替代synchronized关键字或Lock接口来实现更复杂的操作。如果需要进行复合操作,例如原子性的读取-修改-写入操作,仍然需要使用synchronized关键字或Lock接口来保证线程安全性。如 x = y++; 则需要使用 synchronized 将整个语句加锁。

二、使用方法

  • 使用时方法简单,直接在变量定义时添加 volatile 关键字即可,如下

    volatile int count1 = 1;
    private volatile int count2 = 2;
    volatile boolean flag1 = false;
    private volatile boolean flag2 = false;
    

三、测试程序

3.1 验证可见性的示例

  • 在下面示例中,Counter 类有一个 count 变量用于计数,如果不使用 volatile 关键字修饰。在 increment 方法中,两个线程分别对 count 进行自增操作。然后在 Main 类的 main 方法中,创建了两个线程并启动它们,每个线程分别对 Counter 对象的 count 执行1000次自增操作。

  • 由于没有使用 volatile 关键字,线程在执行自增操作时,会将 count 的值从主内存复制到各自的线程工作内存中,进行自增操作后再将结果写回主内存。这可能导致一个线程对 count 的修改无法被另一个线程立即感知到,从而导致计数不准确。

  • 因此,当运行示例时,输出的最终计数结果可能小于2000,因为两个线程之间的自增操作并没有得到正确同步和可见性保证。

  • 相反,如果给 count 变量上添加 volatile 关键字修饰符,可以确保线程之间对该变量的读写操作具有可见性和一致性,从而解决问题。

    package top.yiqifu.study.p004_thread;public class Test051_VolatileVisible {public static class Counter {// 不使用 volatile 关键字private int count = 0;// 使用 volatile 关键字// private volatile int count = 0;public void increment() {count++;}public int getCount() {return count;}}public static void main(String[] args) {Counter counter = new Counter();Thread thread1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.increment();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.increment();}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("最终结果: " + counter.getCount());}}

3.2 验证指令重排序的示例

  • 在下面示例中,Test052_VolatileReorderingExample 类有一个 writer 方法和一个 reader 方法。

    • 在 writer 方法中,首先对变量 x 赋值为1,然后将 flag 设置为 true。
    • 在 reader 方法中,如果 flag 的值为 true,则打印变量 x 的值。
  • 创建一个测试方法 test,在这个方法中创建了两个线程,一个线程执行 writer 方法,另一个线程执行 reader 方法。然后在 main 方法中,创建一个线程用死循环去执行他。

  • 由于变量x和flag没有使用 volatile 关键字,编译器和处理器可能会对指令进行重排序。在不保证顺序性的情况下,可能会发生以下两种重排序情况:

    • 写入操作的重排序:编译器和处理器可能会将写操作2(flag = true)重排序到写操作1(x = 1)之前。
    • 读取操作的重排序:编译器和处理器可能会将读操作2(int a = x)重排序到读操作1(if (flag))之前。
  • 这种重排序可能导致在 reader 方法中打印的变量 a 的值为0,即使在 writer 方法中已经将其设置为1。这是因为在没有足够同步保证的情况下,读操作可能先于写操作执行。

  • 要解决这个问题,可以通过在 x 和 flag 变量上添加 volatile 关键字修饰符,可以防止指令重排序,从而避免这种问题。

  • 下面是测试程序,我在测试时执行了35万次时出现了指令重排序,出现这个问题的概念不是固定的,您测试时需要耐心等待。

    • Test052_VolatileReorderingExample.java 文件内容

      package top.yiqifu.study.p004_thread;public class Test052_VolatileReorderingExample {// 不使用 volatile 关键字private int x = 0;private boolean flag = false;//        // 使用 volatile 关键字
      //        private volatile int x = 0;
      //        private volatile boolean flag = false;public void writer() {x = 1;          // 写操作1flag = true;    // 写操作2}public void reader() {if (flag) {     // 读操作1int a = x;  // 读操作2if(a == 0) {System.out.println("出现了指令重排序,说明先执行了 flag = true,  x = 1 还没有执行");}}}}
    • 测试类 Test052_VolatileOrder.java 内容

      package top.yiqifu.study.p004_thread;public class Test052_VolatileOrder {private static void test(){Test052_VolatileReorderingExample example = new Test052_VolatileReorderingExample();Thread thread1 = new Thread(() -> {example.writer();});Thread thread2 = new Thread(() -> {example.reader();});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) {Thread thread = new Thread(()->{long count = 0;while (true){test();Thread.yield();count++;if(count%10000 == 0){System.out.println("主线程还活着,已执行"+count+"次");}}});thread.start();try {thread.join();} catch (InterruptedException e) {e.printStackTrace();}}}

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

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

相关文章

高德地图点击搜索触发输入提示

减少调用次数&#xff0c;不用每输入一次调用一次&#xff0c;输入完后再触发搜索 效果图&#xff1a; ![Alt](https://img-home.csdnimg.cn/images/20220524100510.png dom结构 <div class"seach"><van-searchshow-actionv-model"addressVal"…

【使用vscode在线web搭建开发环境--code-server搭建】

官方版本下载 https://github.com/coder/code-server/releases?q4.0.0&expandedtrue使用大于版本3.8.0,因为旧版本有插件市场不能访问的情况版本太高需要更新环境依赖 拉取安装包 []# wget "https://github.com/coder/code-server/releases/download/v4.0.0/code-…

探访九牧绿色黑灯工厂,找寻“科技卫浴 世界九牧”的答案

文 | 螳螂观察 作者 | 余一 你所想象中的工厂是怎么样的&#xff1f;灯火通明、人声鼎沸、人来人往&#xff1f;如果告诉你一座工厂既没有灯&#xff0c;也没有人&#xff0c;但却还在持续生产&#xff0c;你会不会觉得这是不可思议的事&#xff1f; 如果不是亲眼见证&#…

Simulink 自动代码生成:手写代码替换生成代码Code Replacement Tool使用

目录 前言 代码替换库操作步骤 代码生成验证 总结 前言 在实际工程开发过程中&#xff0c;Simulink生成的代码都是构建的算法实现的&#xff0c;纯软件实现&#xff0c;生成的代码大多也是直接在CPU上运行的。实际还有一些MCU集成了像Cordic&#xff0c;协处理器等。有些代…

WinEdt 11.1编辑器的尝鲜体验

WinEdt 11.1编辑器的尝鲜体验 2023年5月19日&#xff0c;WinEdt 11.1版本发布了&#xff0c;相比WinEdt 10.3, 最新版更加漂亮&#xff0c;更加友好&#xff0c;更加好用了&#xff01; 最大的改变是WinEdt 11.1 有了自带的WinEdtPDF阅读器&#xff0c;所以不再需要下载第三方…

ros2工作空间

我们先不管ros2工作空间是什么样子的&#xff0c;如果是我自己来搞一个工作空间&#xff0c;我一定是这样安排 一个文件夹用来放自己存放的文件&#xff0c;。。。。。。。。。。对应src文件夹 一个文件夹用来放编译后的文件&#xff0c;。。。。。。。。。。。对应intall文件…

2024测试工程师必学的Jmeter:利用jmeter插件收集性能测试结果汇总报告和聚合报告

利用jmeter插件收集性能测试结果 汇总报告&#xff08;Summary Report &#xff09; 用来收集性能测试过程中的请求以及事务各项指标。通过监听器--汇总报告 可以添加该元件。界面如下图所示 汇总报告界面介绍&#xff1a; 所有数据写入一个文件&#xff1a;保存测试结果到本地…

ZYNQ_project:LCD

模块框图&#xff1a; 时序图&#xff1a; 代码&#xff1a; /* // 24h000000 4324 9Mhz 480*272 // 24h800000 7084 33Mhz 800*480 // 24h008080 7016 50Mhz 1024*600 // 24h000080 4384 33Mhz 800*480 // 24h800080 1018 70Mhz 1280*800 */ module rd_id(i…

解决java在idea运行正常,但是打成jar包后中文乱码问题

目录 比如&#xff1a; 打包命令使用utf-8编码&#xff1a; 1.当在idea中编写的程序,运行一切正常.但是当被打成jar包时,执行的程序会中文乱码.产生问题的原因和解决方案是什么呢? 一.问题分析 分别使用idea和jar包形式打印出System中所有的jvm参数---代码如下: public static…

【设计模式】行为型设计模式

行为型设计模式 文章目录 行为型设计模式一、概述二、责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;三、命令模式&#xff08;Command Pattern&#xff09;四、解释器模式&#xff08;Interpreter Pattern&#xff09;五、迭代器模式&#xff08;Iterato…

Stable Diffusion专场公开课

从SD原理、本地部署到其二次开发 分享时间&#xff1a;11月25日14&#xff1a;00-17&#xff1a;00 分享大纲 从扩散模型DDPM起步理解SD背后原理 SD的本地部署:在自己电脑上快速搭建、快速出图如何基于SD快速做二次开发(以七月的AIGC模特生成系统为例) 分享人简介 July&#…

HelpLook VS Zendesk:哪种知识库软件更适合您的业务

为任何组织创造一个开放且协作的环境至关重要。然而&#xff0c;高水平的员工每周可能会花费多达30个小时处理电子邮件和协作&#xff0c;对他们的工作效率产生了重大影响。 为了解决这个挑战&#xff0c;建立一种高效的信息共享方法至关重要&#xff0c;不会妨碍团队的生产力…

福州大学《嵌入式系统综合设计》实验三:多媒体开发基础编程

一、实验目的 本实验基于搭建好的开发环境和硬件环境&#xff0c;通过编写简单的通信实验&#xff0c;验证开发环境&#xff0c;掌握多媒体开发编程基础&#xff0c;包括SOCKET编程、多线程编程和线程同步知识。 二、实验内容 基于套接字、多线程、同步锁机制实现多媒体文件…

循环链表3

插入函数——插入数据&#xff0c;在链表plsit的pos位置插入val数据元素 位置pos&#xff08;在无特别说明的情况下&#xff09;是从0开始计数的 要改变链表结构&#xff0c;就要依赖前驱&#xff0c;每个前驱的next存储着下一个数据结点的地址&#xff0c;也就是依靠前驱的ne…

netty整合websocket(完美教程)

websocket的介绍&#xff1a; WebSocket是一种在网络通信中的协议&#xff0c;它是独立于HTTP协议的。该协议基于TCP/IP协议&#xff0c;可以提供双向通讯并保有状态。这意味着客户端和服务器可以进行实时响应&#xff0c;并且这种响应是双向的。WebSocket协议端口通常是80&am…

FPGA——IP核 基础操作

FPGA——IP核 基础操作 IP核例化模块时钟IP核RAM IP核 IP核例化模块 找到模版 加入代码中 时钟IP核 配置模式功能 配置输入时钟 输出配置 RAM IP核

海外IP代理科普——API代理是什么?怎么用?

随着互联网的不断发展&#xff0c;越来越多的企业开始使用API&#xff08;应用程序接口&#xff09;来实现数据的共享和交流。而在API使用中&#xff0c;海外代理IP也逐渐普及。那么&#xff0c;什么是API代理IP呢&#xff1f;它有什么作用&#xff1f;API接口有何用处&#xf…

从0开始学习JavaScript--JavaScript 函数

JavaScript中的函数是编写可维护、模块化代码的关键。本文将深入研究JavaScript函数的各个方面&#xff0c;包括基本语法、函数作用域、闭包、高阶函数、箭头函数等&#xff0c;并通过丰富的示例代码来帮助读者更好地理解和应用这些概念。 函数的基本语法 函数是一段可被重复…

openGauss学习笔记-129 openGauss 数据库管理-参数设置-查看参数值

文章目录 openGauss学习笔记-129 openGauss 数据库管理-参数设置-查看参数值129.1 操作步骤129.2 示例 openGauss学习笔记-129 openGauss 数据库管理-参数设置-查看参数值 openGauss安装后&#xff0c;有一套默认的运行参数&#xff0c;为了使openGauss与业务的配合度更高&…

C#学习相关系列之Linq用法---where和select用法(二)

一、select用法 Linq中的select可以便捷使我们的对List中的每一项进行操作&#xff0c;生成新的列表。 var ttlist.select(p>p10); //select括号内为List中的每一项&#xff0c;p10即为对每一项的操作&#xff0c;即对每项都加10生成新的List 用法实例&#xff1a; 1、la…