Java 多线程的三大特性

在JAVA中,线程有原子性、可见性和有序性三大特性。

1.原子性

1.1 定义

对于涉及共享变量的操作,若该操作从其执行线程以外的任意线程来看都是不可分割的,那么我们就说该操作具有原子性。它包含以下两层含义:

访问(读、写)某个共享变量的操作从其执行线程以外的其他任何线程来看,该操作要么已经执行结束要么尚未发生,即其他线程不会看到该操作的中间部分的结果。

访问同一组共享变量的原子操作不能被交错执行。

1.2 非原子性协定

在 Java 语言中,除了 long 类型 和 double 类型以外的任何类型的变量的写操作都是具有原子性的,但对于没有使用  volatile 关键字修饰的 64 位的 long 类型和 double 类型,允许将其的读写操作划分为两次 32 位的操作来进行,这就是 long 和 double 类型的非原子性协定 ( Nonatomic Treatment of double and long Variables ) 。

1.3  保证原子性

通过 Java 虚拟机规范和非原子性协定, Java 语言可以保证对基本数据类型的访问具有原子性,如果想要保证更大范围内的原子性(如多行操作的原子性),此时可以使用字节码指令 monitorenter 和 monitorexit 来隐式执行 lock 和 unlock 操作,从而将串行变成并行来保证原子性;monitorenter 和 monitorexit 这两个字节码指令反映到 Java 代码中就是 synchronized 关键字。

2.可见性

2.1 定义

如果一个线程对某个共享变量进行更新之后,后续访问该变量的其他线程可以读取到这个更新结果,那么我们就称该更新对其他线程可见,反之则是不可见,这种特性就是可见性。出现可见性问题,往往意味着线程读取到了旧数据,这会导致更新丢失,从而导致运行结果与预期结果存在差异。可见性问题与计算机的存储结构和 Java 的内存模型都有着密切的关系。

2.2 高速缓存

由于现代处理器对数据的处理能力远高于主内存(DRAM)的访问速率,为了弥补它们之间在处理能力上的鸿沟,通常在处理器和主内存之间都会存在高速缓存(Cache)。高速缓存相当于一个由硬件实现的容量极小的散列表(Hash Table),其键是一个内存地址,其值是内存数据的副本或者准备写入内存的数据。

现代处理器一般具有多个层次的高速缓存,如:一级缓存(L1 Cache)、二级缓存(L2 Cache)、三级缓存(L3 Cache)等。其中一级缓存通常包含两部分,其中一部分用于存储指令(L1i),另外一部分用于存储数据(L1d)。距离处理器越近的高速缓存,其存储速率越快,制造成本越高,因此其容量也越小。在 Linux 系统中,可以使用 `lscpu` 命令查看其高速缓存的情况

2.3 缓存一致性协议

在多线程环境下,每个线程运行在不同的处理器上,当多个线程并发访问同一个共享变量时,这些线程的执行处理器都会在自己的高速缓存中保留一个该共享变量的副本,这种情况下如何让一个处理器对数据的更改能被其他处理器感知到?为了解决这个问题,需要引入一种新的通讯机制,这就是缓存一致性协议。

缓存一致性协议有着多种不同的实现,这里以广泛使用的 MESI (Modified-Exclusive-Shared-Invalid) 协议为例,和其名字一样,它将高速缓存中的缓存条目分为以下四种状态:

Invalid:该状态表示相应的缓存行中不包含任何内存地址对应的有效副本数据。

Shared:该状态表示相应的缓存行中包含相应内存地址所对应的副本数据,并且其他处理器的高速缓存中也可能存在该相同内存地址所对应的副本数据。

Exclusive:该状态表示相应的缓存行中包含相应内存地址所对应的副本数据,但其他处理器的高速缓存中不应存在该相同内存地址所对应的副本数据,即独占的。

Modified:该状态表示相应的缓存行中包含对相应内存地址所做的更新的结果。MESI 协议限制任意一个时刻只能有一个处理器能对同一内存地址上的数据进行更新,因此同一内存地址在任意一个时刻只能有一个缓存条目处于该状态。

根据以上状态,当某个处理器对共享变量进行读写操作时,其具体的行为如下:

读取共享变量:处理器首先在高速缓存上进行查找,如果对应缓存条目的状态为 M,E 或者 S,此时则直接读取;如果缓存条目为无效状态 I,此时需要向总线发送 Read 消息,其他处理器或主内存则需要回复 Read Response 来提供相应的数据,处理器在获取到数据后,将其存储到相应的缓存条目,并将状态更新为 S 。

写入共享变量:此时处理器首先需要判断是否拥有对该数据的所有权,如果对应缓存条目的状态为 E 或者 M,代表此时均处于独占状态,此时可以直接写入,并将其状态变更为 M 。如果不为 E 或 M,此时处理器需要往总线上发送 Invalidate 消息来通知其他处理器将对应的缓存条目失效,之后在收到其他处理器的 Invalidate Acknowledge 响应后再进行更改,并将其状态变更为 M。

在只有高速缓存的情况下,通过缓存一致性协议能够保证一个线程对共享变量的更新对于其他线程是可见的。如果只是这样,多线程编程就不会存在可见性问题了,但实际上缓存一致性协议并不能保证最终的可见性,这是由于写缓冲器和无效化队列导致的。

2.4 写缓冲器与无效化队列

在上面的缓存一致性协议中,处理器必须等待其他处理器的应答(Read Response \ Invalidate Acknowledge)后才去执行后续的操作,这会带来一定的时间开销,为了解决这个问题,现代计算机架构又引入了写缓冲器和无效化队列:

写缓冲器:当处理器发现缓存条目的状态不为 E 或 M 时,此时不再等待其他处理器返回 Invalidate Acknowledge 消息,而是直接将变更写入到写缓冲器就认为操作完成。当收到对应的 Invalidate Acknowledge 消息,再将变更写入到对应的缓存条目中,此时写操作对于其他处理器而言,才算完成。

无效化队列:当其他处理器接收到 Invalidate 消息后,不再等待删除指定缓存条目中的副本数据后再回复  Invalidate Acknowledge ,而是将消息存入到无效化队列中后就直接回复,之后处理器再根据无效化队列中的消息来重置缓存行的状态到 Invalid 。

写缓冲器是处理器的私有部件,一个处理器的写缓冲器所存储的内容是不能被其他处理器所读取的,这就会导致一个更新即便已经发生并写入到写缓冲器,但是其他处理器上的线程读取到的还是旧值,从而导致可见性问题。除了写缓冲器外,无效化队列也会导致可见性问题,当某个写入发生后,其他处理器上的对应缓存条目应该都立即失效,但是由于无效化队列的存在,Invalidate 操作不会立即执行,导致其他处理器仍然读取到的仍然是未失效的旧值。

2.5 内存屏障

想要解决写缓存器和无效化队列带来的问题,需要引入一个新的机制 —— 内存屏障:

Store Barrier:存储屏障,可以使执行该指令的处理器冲刷其写缓冲器。

Load Barrier:加载屏障,将无效化队列中所指定的缓存条目的状态都标志位 I ,并清空无效化队列,从而保证处理器在读取共享变量时必须发送 Read 消息去获取更新后的值。

冲刷写缓冲器和清空无效化队列都是存在时间消耗的,所以只有在必须要保证可见性的场景下,才应该去使用内存屏障。何种场景下必须要保证可见性,这是由用户来决定的,这也是多线程编程所需要考虑的问题。

2.6  保证可见性

在 Java 语言中,保证可见性的典型实现是 volatile 关键字,它在 Java 语言中一共有三种作用:

保证可见性:Java 虚拟机(JIT 编译器)会在 volatile 变量写操作之后插入一个通用的 StoreLoad 屏障,它可以充当存储屏障来清空执行处理器的写缓冲器;同时 JIT 编译器还会在变量的读操作前插入一个加载屏障来清空无效化队列。

禁止指令重排序:通过内存屏障, Java 虚拟机可以保证 volatile 变量之前的任何读写操作都先于这个 volatile 写操作之前被提交,而 volatile 变量的读操作先于之后任何变量的读写操作被提交。

除了以上两类语义外,Java 虚拟机规范还特别规定了对于使用 volatile 修饰的 64 的 long 类型和 double 类型的变量的读写操作具有原子性。

除了 volatile 外,synchronized 和 final 关键字都能保证可见性:

synchronized :synchronized 关键字规定了对其所修饰的变量执行 unlock 操作前,必须先把此变量同步回主内存中。

final :被 final 修饰的字段在构造器中一旦初始化完成,并且构造器没有把 `this` 的引用逃逸到外部,那么其他线程中就能看到 final 字段的值,即可见性得到保证。

2.7 Java 内存模型

以上主要介绍计算机的内存模型对可见性的影响,但是不同架构的处理器在内存模型和支持的指令集上都存在略微的差异。 Java 作为一种跨平台的语言,必须尽量屏蔽这种差异,而且还要尽量利用硬件的各种特性(如寄存器,高速缓存和指令集中的某些特有指令)来获取更好的执行速度,这就是 Java 的内存模型:

Main Memory:主内存,Java 内存模型规定了所有的变量都存储在主内存中,主内存可以类比为计算机的主内存,但其只是虚拟机内存的一部分,并不能代表整个计算机内存。

Work Memory:工作内存,Java 内存模型规定了每条线程都有自己的工作内存,工作内存可以类比为计算机的高速缓存。工作内存中保存了被该线程使用到的变量的拷贝副本。

线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量;不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

3.有序性

3.1 顺序语义

Java 语言中的顺序语义可以分为以下四类:

源代码顺序:程序员编写的代码的执行顺序;

程序顺序:编译后的代码的执行顺序;

执行顺序:给定代码在处理器上的实际执行顺;

感知顺序:处理器感知到的其他处理器上代码的执行顺序。

3.2 重排序类型

编译器和处理器出于性能考虑,通常会改变代码的实际执行顺序,这种情况就称为重排序,具体分为以下两类:

重排序类型

重排序表现

重排序主体(原因)

指令重排序

程序顺序和源代码顺序不一致

编译器

执行顺序和程序顺序不一致

JIT 编译器、处理器

存储子系统重排序

(内存重排序)

源代码顺序、程序顺序和执行顺序这三者保持一致,

但是感知顺序与执行顺序不一致

高速缓存、写缓冲器

3.3 貌似串行语义

尽管编译器和处理器都能够进行指令重排序,但它们都必须遵循 貌似串行语义(As-if-serial Semantics),即重排序不能影响程序在单线程上执行结果的正确性。按照 As-if-serial 原则,只有不存在数据依赖关系的语句才会被重排序,存在数据依赖关系的语句不会被重排序,示例如下。此时第 1,2 行语句彼此之间可以进行重排序,但是第 3 行语句不能被重排序到 1 和 2 行语句之前:

int a = 1; 
int b = 2; 
int c = a + b; 

同时为了保证单线下执行的正确性,处理器会将重排序指令的执行结果先写入到重排序缓冲器(ROB,Recorder Buffer)中,之后再按照这些指令被处理器读取的顺序提交到寄存器或者主内存中,因此虽然指令是乱序执行的,但结果却是顺序提交的,从而能够保证在单线程下的正确性。

3.4 内存重排序

由于写缓冲器和高速缓存的存在,并且写缓冲器是不能被其他处理器所访问的,因此其他处理器感知到的顺序可能仍然与执行顺序不同,这种情况就叫做内存重排序。但需要说明的是:指令重排序是一种实实在在的重排序,它改变了指令的执行顺序;但内存重排序只是一种现象,只是其他处理器的错觉。

3.5 保证顺序性

在 Java 语言中,volatile 和 synchronized 都能够保证有序性:

volatile:通过内存屏障来禁止指令重排序,通过加载屏障和存储屏障来冲刷写缓冲器和清空无效化队列,从而可以避免内存重排序的现象;

synchronized :使用 synchronized 修饰的变量在同一时刻只允许一个线程对其进行 lock 操作,这种限制决定了持有同一个锁的两个同步块只能串行执行,也就避免了乱序问题。

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

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

相关文章

基于若依的ruoyi-nbcio流程管理系统增加仿钉钉流程设计(六)

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码: https://gitee.com/nbacheng/ruoyi-nbcio 演示地址:RuoYi-Nbcio后台管理系统 这节主要讲条件节点与并发节点的有效性检查,主要是增加这两个节点的子节点检查,因为…

如何在 Unbuntu 下安装配置 Apache Zookeeper

简介 Zookeeper 是 apache 基金组织下的项目,项目用于简单的监控和管理一组服务,通过简单的接口就可以集中协调一组服务,如配置管理,信息同步,命名,分布式协调。 准备工作 Ubuntu 23.04 或者 20.04访问…

pycharm插件推荐:一款能够根据上下文自动提示帮写代码的AI插件

直接上插件: 这个插件有多牛!他能够根据注释帮你直接补全代码(只需要你按一下tab键),甚至还有学习的能力。 如下: 我注释写完后,一回车就模糊的写出了预计的代码,只要我按下tab键…

利用大语言模型(LLM )提高工作效率

日常工作就是面向 google/ 百度编程,除了给变量命名是手动输入,大多时候就是通过搜索引擎拷贝别人的代码,或者找到旧项目一段代码拷贝过来使用。这无疑是开发人员的真实写照;然而,通过搜索引擎搜索答案,无疑…

Reshape.XL 1.2 for Excel插件 Crack

特征 插件 Reshape.XL 包括 130 个基本可组合功能。使用它们,您可以快速轻松地进行非常复杂的数据转换和处理。它们的架构和基本定义受到 SQL 和 R 语言的强烈启发。 到目前为止,类似的功能只能通过脚本语言供程序员使用。借助 Reshape.XL 插件&#xf…

Leetcode-88 合并两个有序数组

使用内置排序函数&#xff0c;时间复杂度On^2 class Solution {public void merge(int[] nums1, int m, int[] nums2, int n) {int j0,im;while(j<n){nums1[i]nums2[j];}Arrays.sort(nums1);} }新建一个临时数组用于放排序后的元素&#xff0c;再将临时数组赋值给nums1&…

linux之按键中断

查看原理图确认引脚 可以看到按键有两个&#xff0c;分别对应GPIO5_1和GPIO4_14 配置pinctrl&#xff0c;配置成GPIO模式 1.使用官方工具&#xff0c;配置下引脚 2.将生成的代码复制到设备树里 创建设备节点 生成二进制设备树文件 在工具链表下使用 make dtbs 或者使…

x264交叉编译(ubuntu+arm)

1.下载源码 https://code.videolan.org/videolan/x264 在windows下解压&#xff1b;复制到ubuntu&#xff1b; 2.进入源码文件夹-新建脚本文件 touch sp_run.sh 3.在sp_run.sh文件中输入 #!/bin/sh./configure --prefix/home/alientek/sp_test/x264/sp_install --enable-…

【python 深拷贝与浅拷贝】

python 深拷贝与浅拷贝 问题&#xff1a; 在用影刀编写流程的时候发现&#xff0c;明明只修改人名为“小张”对应的字典里面的值&#xff0c;但是所有的人名对应的值都被修改了。 原因&#xff1a; 第14行&#xff0c;设置键值对&#xff0c;值对应的变量“初始打卡类型字…

linux下使用vscode对C++项目进行编译

项目的目录结构 头文件swap.h 在自定义的头文件中写函数的声明。 // 函数的声明 void swap(int a,int b);swap.cpp 导入函数的声明&#xff0c;写函数的定义 #include "swap.h" // 双引号表示自定义的头文件 #include <iostream> using namespace std;// 函…

数字化饲料工厂中常见的系统及其介绍

数字化饲料工厂是基于先进技术和数字化平台构建的现代化饲料生产系统&#xff0c;它包含了多种软件、硬件和基础设施系统。以下是数字化饲料工厂中常见的系统及其介绍&#xff1a; 一、自动化控制系统&#xff1a;包括PLC&#xff08;可编程逻辑控制器&#xff09;系统、SCADA&…

【MongoDB】索引 - 单字段索引

MongoDB支持在集合文档中的任意字段上创建索引&#xff0c;默认情况下所有的集合都有一个_id字段的索引&#xff0c;用户和应用可以新增索引用于查询和操作。 一、准备工作 这里准备一些学生数据 db.students.insertMany([{ _id: 1, name: "张三", age: 20, clas…

海康Visionmaster-全局变量:全局变量关联流程中具体 模块结果的方法

将视觉流程中模板匹配算法模块运行的结果数据&#xff1a;特征匹配点 X 关联全局变量 MatchResultX。 在流程运行的主界面中&#xff0c;按照下面 1&#xff0c;2&#xff0c;3&#xff0c;4 步骤操作&#xff0c;第一步选中算法模块&#xff0c;第二步择模块结果 Tab 页&#…

ssm整合原理与实战

文章目录 前言一、SSM整合原理1.1 什么是SSM整合1.2 SSM整合核心问题1.2.1 第一问&#xff1a;SSM整合需要几个IoC容器&#xff1f;1.2.2 第二问&#xff1a;每个IoC容器对应哪些类型组件&#xff1f;1.2.3 第三问&#xff1a;IoC容器之间关系和调用方向&#xff1f;1.2.4第四问…

航模模拟器训练

固定翼吃灰很久忘记咋么操作 故这里发帖防止忘记 准备物品 航模遥控器 aux线 即两端都是耳机插头的线 解密狗 电脑 phoenixRC 航模模拟软件(【飞舜极创】凤凰5.0飞行模拟器 安装和设置方法_哔哩哔哩_bilibili) 链接&#xff1a;https://pan.baidu.com/s/1XL4ZWhMR7MQMxDPC7B…

【Kubernetes 基本概念】Kubernetes 的架构和核心概念

目录 一、Kurbernetes1.1 简介1.2 为什么要用K8s?1.3 K8s的特性 二、Kurbernetes集群架构与组件三、Kurbernetes的核心组件3.1 Master组件3.1.1 Kube-apiserver3.1.2 Kube-controller-manager3.1.3 Kube-scheduler 3.2 配置存储中心——etcd3.3 Node组件3.3.1 Kubelet3.3.2 Ku…

社区牛奶智能售货机为你带来便利与实惠

社区牛奶智能售货机为你带来便利与实惠 低成本&#xff1a;社区牛奶智能货机的最大优势在于成本低廉&#xff0c;租金和人工开支都很少。大部分时间&#xff0c;货柜都是由无人操作来完成销售任务。 购买便利&#xff1a;社区居民只需通过手机扫码支付&#xff0c;支付后即可自…

springboot常见网络相关错误及原因解析

在基于spring-boot开发过程尤其是上线后&#xff0c;经常出现网络相关的错误&#xff0c;令人难以琢磨和下手&#xff0c;所以就spring-boot使用过程中可能碰到的网络相关问题进行分析&#xff0c;结合网络转包、日志报错和前端输出&#xff0c;针对网络连接超时、连接被拒绝、…

谈谈MySQL的底层存储

这个题目启的很大&#xff0c;但其实只是最近在复习MySQL知识的一点心得&#xff0c;比较零散。 更新数据时&#xff0c;底层page的变化 下面这个图&#xff0c;我还需要解释么&#xff1f; 上面的绿色是b数的索引块&#xff0c;分别说明了101号page的最大id是7,102号page的…

时序分解 | Matlab实现FEEMD快速集合经验模态分解时间序列信号分解

时序分解 | Matlab实现FEEMD快速集合经验模态分解时间序列信号分解 目录 时序分解 | Matlab实现FEEMD快速集合经验模态分解时间序列信号分解效果一览基本介绍程序设计参考资料 效果一览 基本介绍 Matlab实现FEEMD快速集合经验模态分解时间序列信号分解 算法新颖小众&#xff0c…