【JMM】保证线程间的可见性,还只知道volatile?

news2024/11/18 9:20:56

本文目录

前言

举例🌰

情形1 int->Integer

情形2 System.out.println()

情形3  storeFence()

情形4  Thread.yield()

情形5  LockSupport.unpark()

情形6  增长循环内代码执行时间

总结分析

volatile分析

字节码解释器实现

模版解释器实现

其他情形分析


前言

这是一篇关于介绍线程间可见性的文章,当然我们先介绍下什么是可见性( ̄∇ ̄)/

可见性指的是一个线程对共享变量的修改对其他线程是可见的

总所周知,volatile 关键字可以解决能够及时可见的问题,使得修改过的数据能立刻被看到,那么只有加 volatile 关键字才能达到这种效果吗?还有什么情况下线程之间是可见的呢?

我们举一个老生常谈的🌰,代码如下

package com.aqin.custom.aqs;

/**
 * @Description
 * @Author aqin1012 AQin.
 * @Date 5/10/23 3:31 PM
 * @Version 1.0
 */
public class TestVisibility {
   private boolean flag = true;
   private int count = 0;

   public static void main(String[] args) throws InterruptedException {
      TestVisibility testVisibility = new TestVisibility();
      Thread threadA = new Thread(() -> testVisibility.methodA());
      threadA.start();
      Thread.sleep(1000);
      Thread threadB = new Thread(() -> testVisibility.methodB());
      threadB.start();
   }

   public void methodA() {
      System.out.println(Thread.currentThread().getName() + "开始循环♻️ ( ̄∇ ̄)/ ……");
      while (flag) {
         count++;
      }
      System.out.println(Thread.currentThread().getName() + "结束循环♻️ \\(^ω^) ! count=" + count);
   }

   public void methodB() {
      flag = false;
      System.out.println(Thread.currentThread().getName() + "修改 flag: " + flag);
   }
}

上述代码执行时,可以看到,卡住了。。。

我等了很久很久很久。。。还是没跳出循环(见下图)

那么在什么情况下上面的methodA方法会跳出循环,输出System.out.println(Thread.currentThread().getName() + "结束循环♻️ \\(^ω^) ! count=" + count)呢?

给变量count加个volatile关键字修饰下,嘿嘿(如下图),跳出循环了

 关于实现多线程之间的可见性,只有volatile吗?

我们来看看其他会使线程之间可见的情形(。・ω・。)ノ

举例🌰

情形1 int->Integer

当循环内的变量count的类型从int变为Integer

 没错,跳出循环(原因最后统一分析)

情形2 System.out.println()

在循环内部添加一行System.out.println()的输出,可以看到,也会结束循环

情形3  storeFence()

添加内存屏障

情形4  Thread.yield()

使用Thread.yield()方法,让出CPU时间片

情形5  LockSupport.unpark()

使用LockSupportunpark方法

情形6  增长循环内代码执行时间

添加如下方法

public static void stopAfter(long interval) {
    long start = System.nanoTime();  //纳秒
    long end;
    do {
        end = System.nanoTime();
    } while (start + interval >= end);
}

设置一个较长的停顿时间

 减小停顿时间

来~我们开始分析,我们先从较为熟悉的volatile开始吧

总结分析

volatile分析

volatile保证线程之间的可见性依靠的是内存屏障

我们先介绍下volatile在hotspot虚拟机中的实现

字节码解释器实现

字节码解释器 Byte Code Interpreter,用C++实现了JVM 指令(每个JVM指令,比如volatile,一般都是由一个C++的函数实现的),优点是简单容易理解,缺点是执行慢

在其中字节码解释器实现的volatile方法中,会先对字段类型判断,然后执行下面这行代码

OrderAccess::storeload()

这是其实就是内存屏障,接下来我们简要的从操作系统层面分析下字节码解释器中的这个实现volatile功能的storeload()

在Linux系统x86架构下,storeload()方法会先对处理器进行判断,看是否是多核处理器(因为单核不存在可见性的问题),如果是就会使用lock(汇编指令)来实现volatile的功能(类似内存屏障的功能)

lock前缀指令的作用

  1. 确保后续指令执行的原子性操作

  2. 类似内存屏障的功能(lock前缀指令不是内存屏障的指令)

  3. 确保其他副本失效

模版解释器实现

模版解释器 Template Interpreter 中其实对每一个常用的指令都写了一段汇编代码,启动时将每个指令与对应的汇编代码入口绑定,以提升效率

volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad | Assembler::StoreStore))

关于StoreLoadStoreStore的理解可以参考下表

屏障类型

指令示例

说明

LoadLoad

Load1;LoadLoad;Load2

保证Load1的读取操作在Load2以及后续读取操作之前执行

StoreStore

Store1;StoreStore;Store2

保证在Store2及其后续写操作执行前,Store1的写操作已经刷新到主内存

LoadStore

Load1;LoadStore;Store2

保证在Store2及其后续写操作执行前,Load1的读操作已经结束

StoreLoad

Store1;StoreLoad;Load2

保证在Load2及其后续的读操作执行前,Store1写操作已经刷新到主内存

不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了底层硬件平台的差异,由JVM来为不同的平台生长相应的机器码,Java中提供了四类内存屏障(上表中的)

内存屏障又称内存栅栏,是一个CPU指令,主要有2个功能:

  • 保证特定操作的执行顺序

  • 保证特定变量的内存可见性(利用该特性实现volatile的内存可见性)

其他情形分析

情形1 中的int->Integer后,会跳出循环的原因是Integer中的intfinal修饰

 在Java虚拟机中,对于使用final关键字修饰的变量或对象引用,在写入操作时会生成相应的内存屏障指令,以确保该变量或对象引用的值对其他线程可见(会在写入操作之后插入一个StoreStore屏障和一个StoreLoad屏障),所以其底层还是通过内存屏障来保证线程之间的可见性的。

情形2 中的System.out.println()println()方法加了synchronized关键字

 在使用synchronized关键字时,JVM会插入不同类型的内存屏障来保证临界区内代码的顺序执行以及变量的可见性和原子性,所以其底层还是通过内存屏障来保证线程之间的可见性的。

情形3 storeFence()就是直接手动添加内存屏障了。

情形4 Thread.yield()是使用Thread.yield()方法让出CPU时间片,即通过上下文切换(需要保存上下文)保证了不同线程之间的可见性。在Java中,从一个线程转到另一个线程,当一个线程被切换出去时,它的本地内存中的数据会被刷新到主内存中,而当另一个线程被切换进来时,它的本地内存会从主内存中加载最新的数据,这个过程保证了不同线程之间的可见性。

需要注意的是,上下文切换虽然可以保证线程之间的可见性,但也会带来一定的开销,因为上下文切换一般需要5-10ms,每次切换都需要将本地内存中的数据刷新到主内存中,这会增加系统的负担。因此,在编写多线程程序时,应该尽量避免过多的上下文切换,以提高程序的性能。

情形5 使用LockSupportunpark方法

 可以看到LockSupport.unpark()底层是通过调用UnSafe类实现的,与情形3⃣️ 不同的是,调用的是unpark()方法,而非storeFence()方法。

情形6 增长循环内代码执行时间,使得缓存过期,再次读取时就会重新去主内存中读取数据,即通过缓存淘汰,保证不同线程之间的可见性。

可以看到其实现不同线程之间的可见性方法有很多,无论是通过内存屏障还是通过上下文切换,其底层原理就是需要满足两点

  • 对线程本地变量的修改可以立刻刷新回主内存

  • 同时使得其他线程中该变量的缓存失效

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

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

相关文章

利用腾讯云cos如何自建一个上传图片源站

目标总纲 对于一个新手来说,做一个东西,最困难的不是怎么做,而是做什么,接下来我会将任务进行拆分,让新手可以轻松"上路"。 在腾讯云上创建一个cos桶上传图片到cos桶在浏览器中如何访问图片(开…

ChatGPT都有些什么好玩的玩法?

ChatGPT是一个智能聊天机器人,可以进行多种有趣的玩法,以下是其中一些: 1. 问答游戏:ChatGPT可以回答各种问题,你可以和它玩问答游戏,看看谁更聪明。 2. 聊天互动:ChatGPT可以进行自然语言聊天…

【C++STL】AVL树(更新中)

前言 二叉搜索树是具有特殊存储结构的树,任意根节点的左子树的所有节点值都比根节点的值小,右子树的所有节点值都比根节点大。 这种特殊的存储结构使得查找的效率大大提升,为logN。但是还有缺陷。 因为二叉搜索树的构建是一个节点一个节点的…

MySQL中去重 distinct 和 group by 是如何去重的

1:测试数据 CREATE TABLE student (stu_no VARCHAR(40) NOT NULL,name VARCHAR(100) NOT NULL );insert into student values(1,name1); insert into student values(2,name2); insert into student values(3,name1); insert into student values(4,name2); i…

【C++初阶】:类与对象(下)

类与对象 一.再谈构造函数1.初始化列表(构造函数的一部分)2.explicit关键字 二.static成员三.友元1.友元函数2.友元类 四.内部类五.匿名对象六.再次理解类与对象 一.再谈构造函数 1.初始化列表(构造函数的一部分) 我们可以直接在…

2023数维杯ABC题思路代码分析

已完成全部可以领取,详情看文末!!! 数维杯A题思路 A题是这次比赛比较难的题目,地下水系统水体污染问题,他给出了我们一些行为特征,污染物的行为特征主要涉及对流迁移、水动力弥散、吸附及阻滞…

一、走进easyUI的世界

1.什么是easyUI? jQuery EasyUI是一组基于jQuery的UI插件集合体,而jQuery EasyUI的目标就是帮助web开发者更轻松的打造出功能丰富并且美观的UI界面。开发者不需要编写复杂的javascript,也不需要对css样式有深入的了解,开发者需要…

matlab实验三程序设计与优化

一、实验目的及要求 一、实验的目的与要求 1、掌握 MATLAB的函数 2、掌握 MATLAB的程序流 3、掌握 MATLAB脚本和函数文件的编写 4、熟悉基于矩阵的程序设计与优化 二、实验原理 1、MATLAB的M文件:脚本文件与函数文件; 2、MATLAB程序流:input…

【企业信息化】第2集 免费开源ERP: Odoo 16 销售管理系统

文章目录 前言一、概览二、使用功能1.通过清晰报价提高销售效率2.创建专业报价单3.管理订单及合同4.简化沟通5.维护产品&价格6.直观的报告7.集成 三、总结 前言 世界排名第一的免费开源ERP: Odoo 16 销售管理系统。通过Odoo Sign应用程序和在线支付,发送报价。…

“极乐净土”时隔7年再度席卷B站,二次元魂藏不住了!

七年前,日本知名歌手美依礼芽发行一曲《极乐净土》,搭配歌曲的宅舞视频燃起了众多二次元魂,翻跳投稿纷至沓来。 可以说,她是许多老二次元人的回忆,也是一个时代的标记。 当时,《极乐净土》横空出世&#…

为什么需要边缘计算?哪些场景需要边缘计算?

为什么需要边缘计算? 边缘计算(Edge Computing)是一种将数据处理和计算功能移到接近数据源头的边缘设备上进行的计算模式。相比传统的云计算模式,边缘计算能够在接近数据源头的地方进行实时的数据处理,这为计算机视觉…

前端vue3一键打包发布

一键打包发布可以分为两种,一是本地代码,编译打包后发布至服务器,二是直接在服务器上拉去代码打包发布至指定目录中。 两种各有使用场景,第一种是前端开发自己调试发布用的比较多,第二种是测试或者其他人员用的多&…

java 二维数组的定义及操作

二维数组的定义有很多方式: 第一种方式: 数据类型[][] 数组名 new数据类型[行的个数][列的个数]; 下面以第一种方式声明一个数组,如下所示。 int[][] xx new int[3][4]; 表示有三行,每行方四个数据第二种方式: 数据类…

甘特图控件DHTMLX Gantt入门使用教程【引入】:dhtmlxGantt 与Node.js(上)

DHTMLX Gantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表。可满足项目管理应用程序的大部分开发需求,具备完善的甘特图图表库,功能强大,价格便宜,提供丰富而灵活的JavaScript API接口,与各种服务器端技术&am…

Vue3-黑马(三)

目录: (1)vue3-基础-计算属性 (2) vue3-基础-xhr-基本使用 (3)vue3-基础-xhr-promise改造 (1)vue3-基础-计算属性 上面有重复的代码,用计算属性&#xff0…

Kali工具集简介

Kali Linux提供了数种经过定制的专门为渗透测试设计的工具。工具都会按下图中下拉选单所示的方式按组分类聚合。了解工具是做渗透测试第一个认知。 口Information Gathering(信息收集) 这些都是侦察工具,用来收集目标网络和设备的数据。在这类工具中,从找出设备的工具到查看使…

大数据分析案例-基于高斯朴素贝叶斯算法构建良恶性肿瘤识别器

🤵‍♂️ 个人主页:艾派森的个人主页 ✍🏻作者简介:Python学习者 🐋 希望大家多多支持,我们一起进步!😄 如果文章对你有帮助的话, 欢迎评论 💬点赞&#x1f4…

时间复杂度和空间复杂度的七七八八

文章目录 时间空间复杂度1. 时间空间复杂度的重要性(作用)2. 时间复杂度和大O表示法1)算法图解2)代码随想录3)王道《数据结构》 3. 大O指的是最糟的情形和一般的情形1)大O表示的是一般情况,并不是严格的上界2&#xff…

算法修炼之练气篇——练气四层

博主:命运之光 专栏:算法修炼之练气篇 前言:每天练习五道题,炼气篇大概会练习200道题左右,题目有C语言网上的题,也有洛谷上面的题,题目简单适合新手入门。(代码都是命运之光自己写的…

创建虚拟目录和用户访问控制+虚拟目录

目录标题 虚拟目录配置文件创建配置文件对应的目录资源在创建一个虚拟目录的配置文件 用户访问控制虚拟目录创建用户访问控制权限创建配置文件中的用户资源测试tom用户测试zhangsan用户 虚拟目录 Alias 虚拟目录名称 真是目录路径为了方便对于网站资源进行灵活管理&#xff0c…