并发编程 可见性、原子性和有序性,如何解决

可见性,原子性和有序性

CPU,内存,I/0

三者在速度上存在很大差异,大概是CPU耗时一天 内存耗时一年,内存耗时一天 /O耗时十年

  1. CPU 增加了缓存,以均衡与内存的速度差异;
  2. 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 //O 设备的速度差异;
  3. 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。

可见性问题

  • 一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。
  • 在单核时代,所有操作都是在一个CPU上。所有线程都是操作同一个CPU 的缓存,一个线程对缓存的写,对另外一个线程来说一定是可见的。 但是在多核时代,每个CPU都会有自己的缓存,就会导致可见性问题。
    比较常见的,定义一个变量初始值为0, 然后一个方法里循环执行10000次 += 操作,同时开启两个线程去执行这个方法。最终变量的值等于20000。这就是可见性问题,因为每个线程计算的变量值都是基于自己CPU缓存中的值。

原子性问题

  • 支持多进程分时复用在操作系统的发展史上却具有里程碑式的意义。 Unix就是因为解决了这个问题而名噪天下的
  • 早期的操作系统是基于进程来调度CPU,不同进程之间是不共享内存空间的,所以在进程切换的时候需要同时切换内存映射地址。而一个进程创建的所有线程,都共享同一个内存空间。所以用线程做任务切换的成本就很低了。 现代操作系统都是基于线程做任务切换。
  • 许多并发编程的BUG就是由于线程切换导致,因为对于高级语言来说,一条语句需要多条CPU指定来完成。比如count +=1,至少需要二条CPU指令。
    1.首先把变量count从内存加载到CPU寄存器
    2.在寄存器中执行 +1 操作
    3.最后把结果写入内存(缓存机制导致可能写入的是CPU缓存,而不是内存。)
    我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性

有序性问题

有序性顾名思义就是有先后顺序。 但是高级语言编译器,在编译的过程中为了优化性能,可能会改变程序中语句的先后顺序。“a=6:b=7:”编译器优化后可能变成“b=7:a=6:”但是这个例子里 不会对程序造成影响。
在这里插入图片描述
看起来上述代码很完美。但是是有问题的。
问题出现在 new 操作上
我们认为的 new操作
1.分配一块内存 M;2.在内存 M 上初始化 Singleton 对象;3.然后 M 的地址赋值给 instance 变量.
优化后的new操作
1.分配一块内存 M;2.将 M 的地址赋值给 instance 变量;3.最后在内存 M 上初始化 Singleton 对象。

小结

其实对于可见性,原子性,有序性这三个问题,最初的目的都是为了提高性能,但是技术在解决一个问题的时候一定会带来另一个问题,所以在选
择的时候,一定要知道会带来什么问题,以及如何规避。

Java如解决可见性和有序性问题

问题

1.导致可见性问题是由于 CPU缓存 2.导致有序性问题是由于编译优化

解决

最直接的办法就是禁用CPU缓存和编译优化,但是这样会影响到我们程序的效率,那么就需要根据需求来禁用。

Java内存模型

  • Java内存模型不是一个真的“模型”,而是一个很复杂的规范。 通俗一点说就是 Java内存模型JVM 如何提供按需禁用缓存和编译优化的方法。
  • 这些方法包括了 volatile,synchronized 和 final 三个关键字,以及多项 Happens-Before 规则
  • volatile
    这个关键字的作用主要是 告诉编译器,这个变量的读写不能使用CPU缓存,必须从内存中读取或者写入
    举例子
    在这里插入图片描述
    原因
  • 在jdk 1.5以后有了-项 happen before规则
    网上翻译叫 先行发生。 准确的含义是 前面一个操作的结果对后续操作是可见的。
    比较正式的说法是:Happens-Before约束了编译器的优化行为,虽然云溪编译器优化,但是要求编译器优化后也一定道守 Happens-Before 规则
  • Happens-Before 规则
    volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见,
    程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变。
    传递性规则:这个简单的,就是happens-before原则具有传递性,即hb(A,B),hb(B,C),那么hb(A,C)。
    管程锁的规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现) --【假设x的初始值是 10,线程 A 执行完代码块后x的值会变成12(执也就是线程 B 能够看到 x==12.】
    线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
    在这里插入图片描述
    线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。也称线程join(0规则。
    在这里插入图片描述
    线程中断规则:对线程interrupt0方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted0检测到是否发生中断。
    对象终结规则:这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定happens-before它的finalize0方法。

如何解决原子性问题

原子性问题的源头是线程切换。线程切换是依赖CPU中断的,所以禁止CPU发生中断就能禁止线程切换。 当然这种方式在单核时代是可行的,但是多核时代却并不适合。

举例子: 32位CPU上写Long型变量,这个操作会被拆分成两次写操作,(写高32位和 低32位)
单核CPU场景下,同一时刻只有一个线程执行,禁止CPU中断就意味着操作系统不会重新调度线程,所以这两次操作就是都被执行或者都没有被执行,具有原子性。
多核CPU场景下,同一时刻可能有两个线程在同时执行,一个CPU-1,一个咋CPU-2上,所以并不能保证同一时刻只有一个线程执行,如果同时写高32位 就会出现诡异bug了。
同一时刻只有同一个线程执行,这个条件很重要,我们一般称为 互斥。如果不管是单核还是多核CPU我们能够保证共享变量的修改是互斥的,那么就能保证原子性了。

临界区

我们把一段需要互斥执行的代码称为临界区。进入临界区之前枷锁,如果成功就进入临界区,此时线程持有锁。否则就等待,直到持有锁的线程解锁。持有锁的线程执行完临界区代码后,执行解锁操作。

锁模型

在现实生活中,锁和要保护的资源是有联系的,比如你家里的锁保护你家里的东西。我家里的锁保护我家里的东西。在并发编程世界里,锁和资源的也应该有这样的关系。所以我们需要完善一下模型,
首先我们要把临界区要保护的资源标注出来。资源R,同时需要创建一个锁LR。此时LR 用来会保护R,这个关系比较重要,容易出现自己门
他家资产的事情。

synchronized

在这里插入图片描述
当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X;
在这里插入图片描述
当修饰非静态方法的时候,锁定的是当前实例对象 this。在这里插入图片描述

解决问题

count+=1 问题
在这里插入图片描述
addOne0 方法,被关键字修饰后,无论是单核还是多核CPU只有一个线程能够执行addOne0,所以一定能保证原子操作,就不会有可见性问题。

管程中锁的规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。 – 管程,就是我们这里的synchronized(至于为什么叫管程,我们后面介绍),我们知道 synchronized 修饰的临界区是互斥的,也就是说同一时刻只有一个线程执行临界区的代码;而所谓“对一个锁解锁 Happens-Before 后续对这个锁的加锁”,指的是前一个线程的解锁操作对后一个线程的加锁操作可见,综合 Happens-Before 的传递性原则,我们就能得出前一个线程在临界区修改的共享变量(该操作在解锁之前),对后续进入临界区(该操作在加锁之后)的线程是可见的。

但可能会忽略了 get0方法。执行 addOne0 方法后,value 的值可见性是没法保证的。管程中锁的规则,是只保证后续对这个锁的加锁的可见性
而 get0) 方法并没有加锁操作,所以可见性没法保证。
解决方法,给get()方法也加锁…
在这里插入图片描述
这两个关键字保护的资源都是 this,当前对象

锁和受保护资源的关系

受保护资源和锁之间的关联关系非常重要,他们的关系是怎样的呢?一个合理的关系是:受保护资源和锁之间的关联关系是 N:1的关系。 显示时间中可以用多把锁保护同一个资源,但是在并发领域是不行的。
上面的代码稍作改变
在这里插入图片描述
改动后的代码是用两个锁保护一个资源。这个受保护的资源就是静态变量 value,两个锁分别是 this 和 SafeCalc.class。我们可以用下面这幅图来形象描述这个关系。由于临界区 get0 和 addOne0) 是用两个锁保护的,因此这两个临界区没有互斥关系,临界区 addOne0) 对 value 的修改对临界区 get0 也没有可见性保证,这就导致并发问题了。

问题

在这里插入图片描述
在这里插入图片描述

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

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

相关文章

力扣第541题: 反转字符串 II

题目: 给定一个字符串 s 和一个整数 k,从字符串开头算起, 每计数至 2k 个字符,就反转这 2k 个字符中的前 k 个字符。 如果剩余字符少于 k 个,则将剩余字符全部反转。 如果剩余字符小于 2k 但大于或等于 k 个,则反转…

迎接大模型时代:程序员的挑战与应对

随着人工智能技术的迅猛发展,大模型已成为当前人工智能领域的重要趋势之一。大模型具有更强大的表征能力和泛化能力,已在自然语言处理、计算机视觉等领域取得了显著成果。然而,大模型的出现也给程序员带来了新的挑战和机遇。 大模型对程序员…

通过v-if渲染的element-ui表单,校验规则不生效的问题

因为form-item绑定验证事件是在mounted中进行的,规则变化后没有进行重新绑定验证事件,v-if渲染组件节点diff后被复用了,所以验证也就自然失效了 例如:通过动态选择类型来控制驾驶人是否显示,并且是必填项 给每一个el…

创建Vue项目后的初始化操作-解决Vue项目中盒子高度100%不生效问题

解决Vue项目中盒子高度100%不生效问题。 (由于最近create的项目有点多,记录一下)。 文章目录 方法一:对症下药方法二:偏方补充 方法一:对症下药 在项目根目录/public/index.html文件中的head里加入以下代码…

Java 与垃圾回收有关的方法

1. gc 调用垃圾回收器的方法是 gc,该方法在 System 类和 Runtime 类中都存在。 在 Runtime 类中,方法 gc 是实例方法,方法 System.gc 是调用该方法的一种传统而便捷的方法。在 System 类中,方法 gc 是静态方法,该方法…

oracle一次sql优化笔记

背景:两个百万级数据量表需要连接,加全索引的情况下速度仍不见改善,苦查一下午解决问题未遂。 解决:经大佬指点了解到oracle优化器提示,使用/* USE_HASH(table1 table2) */或者/* USE_MERGE(table1 table2) */来指导优…

P5732 【深基5.习7】杨辉三角

此题可以为杨辉三角&#xff0c;可以看一下这篇文章: 传送门 AC代码&#xff1a; #include<iostream>using namespace std;const int N 30; int arr[N][N];int main() {int n;cin >> n ;arr[1][1] 1;for(int i1;i<n;i){for(int j1;j<i;j){if(j 1 || j …

括号成对匹配

括号成对匹配 题目 括号成对匹配。例如&#xff1a;[a{b©d}e]匹配成功&#xff0c;a(b}匹配不成功 思路 这题的考察点是栈的原理&#xff0c;可以把括号匹配看成入栈和出栈&#xff0c;如果是左边的括号一律入栈&#xff1b;如果是右括号需要先与对应的左括号进行对比…

Callable and FutureTask

Callable 由关系图可知&#xff0c;Callable和Runnable一样&#xff0c;也是一个函数式接口&#xff0c;可以使用Lambda表达式 与之不同的是&#xff0c;其内部的call()方法可以抛出异常且能return一个返回值 Callable<Object> callable new Callable() {Overridepublic…

网上赚钱新姿势:日赚二三十,十大靠谱平台任你选!

互联网时代下&#xff0c;网络兼职已成为许多人追求额外收入的热门选择。互联网的广泛普及与发展&#xff0c;不仅让人们轻松获取海量信息&#xff0c;更为我们提供了多样化的兼职机会。这些兼职工作不仅时间自由&#xff0c;而且种类繁多&#xff0c;适合各种人群参与。接下来…

JavaWeb 监听器

Javaweb监听器是一种特殊的类&#xff0c;用于监听Web应用程序中的事件和对象。它可以监听Web应用程序的启动和关闭事件&#xff0c;会话的创建和销毁事件&#xff0c;以及请求和响应的事件等。监听器可以在特定事件发生时执行一些特定的操作&#xff0c;比如记录日志、初始化资…

【AR开发示例】实现AR管线巡检

写在前面的话 这是一篇旧文档&#xff0c;代码仓库见 https://gitee.com/tanyunxiu/AR-pipe 本文档是基于超图移动端SDK的AR模块开发的示例&#xff0c;仅供参考&#xff0c;SDK在持续迭代中&#xff0c;相关描述可能有变化。 示例介绍 这是一个使用AR查看墙内管线的基础示…

Ant Design中Tree使用defaultExpandAll属性后不会默认展开所有节点怎么办?

最近做前端项目时&#xff0c;使用到了 tree 组件&#xff0c;选择使用 Ant Design 中的 tree 组件&#xff0c;默认所有节点初始时全部展开&#xff0c;使用 defaultExpandAll 属性。但是显示的时候&#xff0c;一个节点都没展开。于是调研了一下这个问题。发现有以下问题&…

leetcode刷题(python)——(六)

01.03.07 练习题目&#xff08;第 06 天&#xff09; 1. 0506. 相对名次 1.1 题目大意 描述&#xff1a;给定一个长度为 n n n 的数组 s c o r e score score。其中 s c o r e [ i ] score[i] score[i] 表示第 i i i 名运动员在比赛中的成绩。所有成绩互不相同。 要求&…

在 Vue中,v-for 指令的使用

在 Vue中&#xff0c;v-for 指令用于渲染一个列表&#xff0c;基于源数据多次渲染元素或模板块。它对于展示数组或对象中的数据特别有用。 数组渲染 假设你有一个数组&#xff0c;并且你想为每个数组元素渲染一个 <li> 标签&#xff1a; <template> <ul>…

Spring Cloud 运维篇1——Jenkins CI/CD 持续集成部署

Jenkins 1、Jenkins是什么&#xff1f; Jenkins 是一款开源 CI/CD 软件&#xff0c;用于自动化各种任务&#xff0c;包括构建、测试和部署软件。 Jenkins 支持各种运行方式&#xff0c;可通过系统包、Docker 或者一个独立的 Java 程序。 Jenkins Docker Compose持续集成流…

k8s安装,linux-ubuntu上面kubernetes详细安装过程

官方文档&#xff1a;https://kubernetes.io/zh-cn/docs/setup/production-environment/container-runtimes/ 环境配置 该部分每个主机都要执行 如果你确定不需要某个特定设置&#xff0c;则可以跳过它。 设置root登录 sudo passwd root sudo vim /etc/ssh/sshd_config Perm…

HarmonyOS NEXT 使用XComponent + Vsync 实现自定义动画

介绍 XComponent 提供了应用在 native 侧调用 OpenGLES 图形接口的能力&#xff0c;本文主要介绍如何配合 Vsync 事件&#xff0c;完成自定义动画。在这种实现方式下&#xff0c;自定义动画的绘制不在 UI 主线程中完成&#xff0c;即使主线程卡顿&#xff0c;动画效果也不会受…

晶圆制造之MPW(多项目晶圆)简介

01、MPW是什么&#xff1f; 在半导体行业中&#xff0c;MPW 是 "Multi Project Wafer" 的缩写&#xff0c;中文意思是多项目晶圆。MPW 的主要思想是将使用相同工艺的多个集成电路设计放在同一晶圆片上进行流片&#xff08;即制造&#xff09;。这种方法允许多个设计共…

java学习笔记6

11. 类的封装 ​ 在Java中,**封装(Encapsulation)**是面向对象编程中的重要概念,它指的是将类的数据(属性)和行为(方法)绑定在一起,并对外部隐藏数据的具体实现细节,只通过公共方法来访问和操作数据。这有助于提高代码的安全性、可维护性和灵活性。 11.1 为什要封装 …