【Java 并发】三大特性

在 Java 的高并发中,对于线程并发问题的分析通常可以通过 2 个主核心进行分析

  1. JMM 抽象内存模型和 Happens-Before 规则
  2. 三大特性: 原子性, 有序性和可见性

JMM 抽象内存模型和 Happens-Before 规则, 前面我们讨论过了。这里讨论一下三大特性。

1 原子性

定义: 一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断要么就都不执行

简单地说就是一个操作被定义为原子性了, 那么不管这个操作里面包含多少个步骤, 他们都是一个整体的, 这个整体只有执行成功, 或不执行, 不存在任何的中间态, 比如只执行一半, 或者一半成功, 一半失败。

而回到 Java 中, 很多操作看起来就一个操作, 好像是具备原子性, 但是实际中却不具备原子性。

猜猜下面的操作哪些是原子操作?

// 1
int a = 1;// 2
a++;// 3
int b = a + 1;// 4
a = a + 1;

答案是: 只有第一个。

a++ 操作可以拆分为下面 3 步:

  1. 读取 a 的值
  2. a 的值加 1
  3. 将计算后的值重新赋值给 a

其他 2 个的分析类似。

1.1 原子操作

在 Java 内存模型中定义了 8 种原子操作

操作作用对象说明
lock (锁定)主内存中的变量把一个变量标识为一个线程独占的状态
unlock (解锁)主内存中的变量把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取)主内存的变量把一个变量的值从主内存传输到线程的工作内存中,以便后面的 load 动作使用
load (载入)工作内存中的变量把 read 操作从主内存中得到的变量值放入工作内存中的变量副本
use (使用)工作内存中的变量把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
assign (赋值)工作内存中的变量把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
store (存储)工作内存的变量把工作内存中一个变量的值传送给主内存中以便随后的write操作使用
write (操作)主内存的变量把 store 操作从工作内存中得到的变量的值放入主内存的变量中

上面的这些指令操作是相当底层的,可以作为扩展知识面掌握下。

需要注意的一点就是: 指令与指令之间组合起来达到某个效果。
但是 Java 内存模型只要求这些组合指令之间是顺序执行的,不强制他们一定是连续执行的。

比如把一个变量从主内存中复制到工作内存中就需要执行 read, load 操作,将工作内存同步到主内存中就需要执行 store, write 操作。
也就是说 read 和 load 之间可以插入其他指令,store 和 writer 可以插入其他指令。
比如对主内存中的 a, b 进行访问就可以出现这样的操作顺序: read a, read b, load b, load a

支持变量操作的原子操作的有 read, load, use, assign, store, write。 基础数据类型比较简单, 基本只有使用到其中的一条, 所以可以看为基本数据类型的访问读写具备原子性 (long 和 double 的操作不具备操作性), 如上面的 int a = 1;

1.2 synchronized 和 volatile 对原子性的支持

synchronized

上面一共有八条原子操作,其中六条可以满足基本数据类型的访问读写具备原子性,还剩下 lock 和 unlock 两条原子操作。这 2 个可以用于支持更大范围的原子性操作。
尽管 JVM 没有把 lock 和 unlock 开放给我们使用,但 JVM 以更高层次的指令 monitorenter 和 monitorexit 指令开放给我们使用,
映射到 Java 代码中就是— synchronized 关键字, 也就是说 synchronized 满足原子性。

public void test() {int b = 0;synchronized(Test.class) {int a = 1;b = a;}
}

volatile

而说到关键字, Java 中另一个和并发相关的高频关键字 volatile, 是否可以保证原子性了。
先举一个例子。

public class VolatileExample {private static volatile int counter = 0;public static void main(String[] args) {// 启动 10 个线程for (int i = 0; i < 10; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10000; i++)counter++;}});thread.start();}try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(counter);}
}

如上: counter 是 volatile 修饰的。
开启 10 个线程,每个线程都自加 10000 次,如果不出现线程安全的问题最终的结果应该就是:10 * 10000 = 100000。
但是运行多次都是小于 100000 的结果, 也就是说明: volatile 不能保证原子性

从上面的说明可以知道 counter++ 不是原子性操作。如果线程 A 读取 counter 到工作内存后,其他线程对这个值已经做了自增操作后,那么线程 A 的这个值自然而然就是一个过期的值,因此,总结果必然会是小于 100000 的。

如果让 volatile 保证原子性,必须符合以下两条规则:

  1. 运算结果并不依赖于变量的当前值,或者能够确保只有一个线程修改变量的值
  2. 变量不需要与其他的状态变量共同参与不变约束 (volatile 变量的变化不会与其它变量的变化有任何联系)

2 有序性

定义: 程序执行的顺序按照代码的先后顺序执行。

例如:

int i = 0;              
boolean flag = false;
i = 1;                // 语句1  
flag = true;          // 语句2

先给变量 i 赋值,然后给 flag 赋值,语句 1 在 语句 2 的前面。
但是在编译器和处理器可能会为了性能对其进行重排序 (指令重排序), 因为语句 1 和 语句 2 之间没有依赖关系,所以语句 2 可能被重排序到语句 1 的前面。

2.1 synchronized 和 volatile 对有序性的支持

synchronized

synchronized 语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。
因此, synchronized 语义就要求线程在访问读写共享变量时只能 “串行” 执行, 因此 synchronized 具有有序性。

但是在 synchronized 内部的代码块的逻辑, JVM 没有禁止重排序, 也就是支持处理器为了性能, 在不影响结果的情况下, 调整执行顺序。

例子:

public void test() {int b = 0;synchronized(Test.class) {int a = 1;// 1b = a;// 2int c = a;}
}

上面 1,2 2 个操作没有存在结果的依赖, 如果为了性能, 处理器仍然可以对他们进行重排序, 变为 2, 1 的执行顺序。

volatile

在 Java 内存模型中说过,为了性能优化,编译器和处理器会进行指令重排序。
也就是说 Java 程序天然的有序性可以总结为:如果在本线程内观察,所有的操作都是有序的, 如果在一个线程观察另一个线程,所有的操作都是无序的。那么 volatile 具备有序性吗?

先看一个例子:volatile 和 双重检验锁定的方式(Double-checked Locking)的关系:

public class Singleton {private volatile static Singleton instance;private Singleton() { }public Singleton getInstance(){if(instance == null){synchronized (Singleton.class){if(instance == null){instance = new Singleton();}}}return instance;}
}

这里的 instance 为什么要加 volatile 修饰?
创建一个对象,实际是经过 3 步实现的

  1. 分配对象内存空间
  2. 初始化对象
  3. 将对象指向我们刚刚分配的内存

但是不加 volatile 在重排序的作用下, 可能会出现下面的执行顺序:

Alt 'ObjectInitWithoutVolatile'

如果 2 和 3 进行了重排序的话,线程 B 进行判断 if( instance == null) 时就会为 true,而实际上这个 instance 并没有初始化成功,显而易见对线程 B 来说之后的操作就会是错的。
而用 volatile 修饰的话,可以禁止 2 和 3 操作重排序,从而避免这种情况。volatile 包含禁止指令重排序的语义, 其具有有序性。

3 可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

// 线程 1 执行的代码
int i = 0;
i = 10;// 线程 2 执行的代码
j = i;

假若执行线程 1 的是 CPU1,执行线程 2 的是 CPU2。由上面的分析可知:
当线程 1 执行 i = 10 这句时,会先把 i 的初始值加载到 CPU1 的本地缓存,然后赋值为 10,那么在 CPU1 的本地缓存中把 i 的值变为 10,却没有立即写入到主存当中。

此时线程 2 执行 j = i,它会先去主存读取 i 的值并加载到 CPU2 的本地缓存当中,此时内存当中 i 的值还是 0,那么就会使得 j 的值为 0,而不是 10。
这就是可见性问题,线程 1 对变量 i 修改了之后,线程 2 没有立即看到线程 1 修改的值。

3.1 synchronized 和 volatile 对有序性的支持

synchronized
通过对 synchronized 的内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。所以 synchronized 具有可见性 (Happens-Before 的 Monitor 规则保证)。

volatile

volatile 修饰的变量在写的时候,底层会在后面在添加 lock 指令,确保将修改的值刷新到主内存,所以 volatile 具备可见性。

4 总结

synchronized 具有: 原子性, 有序性, 可见性。

volatile 具有:有序性,可见性。

synchronized 是否保证有序性呢? 从上面的双重检测看起来, synchronized 貌似不保证有序性, 但是 synchronized 还是保证有序性的, 只是和 volatile 的有序性不一样。

volatile 关键字禁止 JVM 编译器和处理器对其进行重排序, 而 synchronized 保证的有序性是只有单线程可以获取锁, 串行地执行同步代码的结果, 但是同步代码里的语句是会发生指令重排序。
进入 synchronized 代码块前, 底层先添加一个 acquire barrier, 在最后添加一个 release barrier, 保证同步代码块中的代码不能和同步代码块外面的代码进行指令重排, 在其内部还是会发生指令重排但基本不会影响结果。


public void testSynchronized() {// 1int a = 1;// 2synchronized(TestDemo.class) {// 2.1a = 2;// 2.2int b = 3;// 2.3int c = 4;}// 3a = 3;
}

将 synchronized 内的代码块看着整个整体, 在 synchronized 的作用下, 1, 2, 3 是有序的,
但是 synchronized 不保证代码块内的代码是有序的, 在没有数据依赖的条件下, 运行指令重排序, 也就是可能存在 2.1 - 2.3 - 2.2 等情况。

5 参考

三大性质总结:原子性、可见性以及有序性

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

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

相关文章

第十一章 算法复杂度

11.1 大O表示法 它用于描述算法的性能和复杂程度。分析算法时&#xff0c;时常遇到以下几类函数&#xff1a; 11.1.1 理解大O表示法 如何衡量算法的效率&#xff1f;通常是用资源&#xff0c;例如CPU&#xff08;时间&#xff09;占用、内存占用、硬盘占用和网络 占用。当讨论…

时序预测 | Python实现GRU电力需求预测

时序预测 | Python实现GRU电力需求预测 目录 时序预测 | Python实现GRU电力需求预测预测效果基本描述程序设计参考资料预测效果 基本描述 该数据集因其每小时的用电量数据以及 TSO 对消耗和定价的相应预测而值得注意,从而可以将预期预测与当前最先进的行业预测进行比较。使用该…

同义词替换在论文降重中的实际效果评估 快码论文

大家好&#xff0c;今天来聊聊同义词替换在论文降重中的实际效果评估&#xff0c;希望能给大家提供一点参考。 以下是针对论文重复率高的情况&#xff0c;提供一些修改建议和技巧&#xff0c;可以借助此类工具&#xff1a; 标题&#xff1a;同义词替换在论文降重中的实际效果评…

PMI相关证书的获取步骤及注意内容

近几年很多行业的从业人员都在考取PMI项目管理相关证书&#xff0c;可在中国大陆地区参加考试的认证主要有&#xff1a;PMP, PgMP, PMI-RMP, PMI-ACP, PMI-PBA, CAPM。PfMP, PMI-SP尚未在中国大陆地区开放考试。 现整理该类证书的相关获取步骤及注意内容 一、证书获取步骤 S…

RHEL8_Linux下载ansible

本章内容主要介绍RHEL8中如何安装ansible ansible时如何工作的在RHEL8中安装ansible 1.ansible工作原理 如果管理的服务器很多&#xff0c;如几十台甚至几百台&#xff0c;那么就需要一个自动化管理工具了&#xff0c;ansible就是这样的一种自动化管理工具。 1&…

将html的radio单选框自定义样式为正方形和对号

将html的radio单选框自定义样式为正方形和对号 背景&#xff1a; 如何能把html的<input type"radio" name"option">改成自定义的样式呢&#xff1f;比如想要把他变成正方形&#xff0c;选中的时候是对号。默认的样式太丑了 默认样式&#xff1a; 自…

2023前端面试题总结:JavaScript篇完整版

前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 JavaScript基础知识 JavaScript有哪些数据类型&#xff0c;它们的区别&#xff1f; Number&#xff08;数字&#xff09;: 用于表示数值&#xff0c;可…

LLM Agent发展演进历史(观看metagpt视频笔记)

LLM相关的6篇重要的论文&#xff0c;其中4篇来自谷歌&#xff0c;2篇来自openai。技术路径演进大致是&#xff1a;SSL (Self-Supervised Learning) -> SFT (Supervised FineTune) IT (Instruction Tuning) -> RLHF。 word embedding的问题&#xff1a;新词如何处理&…

【二分查找】自写二分函数的总结

作者推荐 【动态规划】【广度优先搜索】LeetCode:2617 网格图中最少访问的格子数 本文涉及的基础知识点 二分查找算法合集 自写二分函数 的封装 我暂时只发现两种&#xff1a; 一&#xff0c;在左闭右开的区间寻找最后一个符合条件的元素&#xff0c;我封装成FindEnd函数。…

Linux---进程概念

目录 一、冯诺依曼体系结构 二、操作系统 1.关于下三层的理解 2.关于上三层的理解 三、进程 1.进程(也叫做任务)对应的标识符---pid 2.fork---用代码创建进程(系统接口) 1&#xff09;初步认识一下fork 2&#xff09;fork函数的返回值 3&#xff09;fork的原理 问题1…

虚拟机性能监控、故障处理工具

虚拟机性能监控、故障处理工具 二、基础故障处理工具4.2.1 jps&#xff1a;虚拟机进程状况工具4.2.2 jstat:虚拟机统计信息监视工具4.2.3 jinfo:Java配置信息工具4.2.4 jmap:java内存映像工具4.2.5 jhat:虚拟机堆转储快照分析工具4.2.6 jstack&#xff1a;Java堆栈跟踪工具4.2.…

四舍五入浮点数

1.题目如下&#xff1a; 2.方法一&#xff1a; 直接取出小数部分第一位来判断。 1. 先乘以10。 2. 强制类型转换为整型&#xff0c;去掉小数部分。 3. 再模10&#xff0c;相当于取出原数的小数第一位。 代码实现&#xff1a; int way1(double n) {int a (int)(n * 10);int b…

后端开发——统一处理异常Spring MVC机制

一、Spring MVC的统一处理异常机制 在Spring MVC中&#xff0c;存在统一处理异常的机制&#xff0c; 具体表现为&#xff1a;无论是哪个处理请求的过程中出现异常&#xff0c;每种类型的异常只需要编写一段处理异常的代码即可&#xff01; 统一处理异常的核心是定义处理异常的…

【k8s】使用Finalizers控制k8s资源删除

文章目录 词汇表基本删除操作Finalizers是什么&#xff1f;Owner References又是什么&#xff1f;强制删除命名空间参考 你有没有在使用k8s过程中遇到过这种情况: 通过kubectl delete指令删除一些资源时&#xff0c;一直处于Terminating状态。 这是为什么呢&#xff1f; 本文将…

智能优化算法应用:基于静电放电算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于静电放电算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于静电放电算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.静电放电算法4.实验参数设定5.算法结果6.…

FME之FeatureReader转换器按表格内容读取矢量数据

问题&#xff1a;平时会遇到只用某个大数据里某小部分数据参与下一步数据处理&#xff0c;此时我们会用到FeatureReader转换器&#xff0c;一般是通过空间关系&#xff08;相交、包含&#xff09;来读取相应涉及的图斑矢量&#xff0c;但就有一个问题&#xff0c;加入你的启动器…

计算机视觉(P2)-计算机视觉任务和应用

一、说明 在本文中&#xff0c;我们将探讨主要的计算机视觉任务以及每个任务最流行的应用程序。 二、图像内容分类 2.1. 图像分类 图像分类是计算机视觉领域的主要任务之一[1]。在该任务中&#xff0c;经过训练的模型根据预定义的类集为图像分配特定的类。下图是著名的CIFAR…

格式化Echarts的X轴显示,设置显示间隔

业务需求&#xff1a;x轴间隔4个显示&#xff0c;并且末尾显示23时 x轴为写死的0时-23时&#xff0c;使用Array.from data: Array.from({ length: 24 }).map((_, i) > ${i}时) 需要在axisLabel 里使用 interval: 0, // 强制显示所有刻度标签&#xff0c;然后通过 formatter …

分面中添加不同表格

简介 关于分面的推文&#xff0c;小编根据实际科研需求&#xff0c;已经分享了很多技巧。例如&#xff1a; 分面一页多图 基于分面的面积图绘制 分面中的细节调整汇总 分面中添加不同的直线 基于分面的折线图绘制 最近遇到了另一个需求&#xff1a;在分面中添加不同的表…

计算机网络(四)

九、网络安全 &#xff08;一&#xff09;什么是网络安全&#xff1f; A、网络安全状况 分布式反射攻击逐渐成为拒绝攻击的重要形式 涉及重要行业和政府部门的高危漏洞事件增多。 基础应用和通用软硬件漏洞风险凸显&#xff08;“心脏出血”&#xff0c;“破壳”等&#x…