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

可见性,原子性和有序性

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,一经查实,立即删除!

相关文章

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 …

Callable and FutureTask

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

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

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

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

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

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 为什要封装 …

Python exe 文件反编译为 Python 脚本

文章目录 前言版本反编译Python 可执行文件&#xff08;.exe&#xff09;反编译打包一个简单的 .exe 可执行文件提取 pyc 文件使用脚本提取使用工具提取 将 .pyc 文件转换为 Python 脚本入口运行类非入口运行类转换补全后的 pyc 文件uncompyle6 反编译在线工具 可能遇到的问题P…

如何在在wordpress安装百度统计

前言 看过我的往期文章的都知道&#xff0c;我又建了一个网站&#xff0c;这次是来真的了。于是&#xff0c;最近在查阅资料时发现&#xff0c;有一款免费的软件可以帮我吗分析网站数据。&#xff08;虽然我的破烂网站压根没人访问&#xff0c;但是能装上的都得上&#xff0c;…

探索边缘计算:技术的新疆界

探索边缘计算&#xff1a;技术的新疆界 在当今迅速发展的数字化时代&#xff0c;云计算作为数据处理的主力军已广泛应用。但是&#xff0c;随着物联网&#xff08;IoT&#xff09;设备的急剧增加和数据生成速率的加快&#xff0c;云计算面临着种种挑战。边缘计算因此诞生&…

STL-list的使用及其模拟实现

在C标准库中&#xff0c;list 是一个双向链表容器&#xff0c;用于存储一系列元素。与 vector 和 deque 等容器不同&#xff0c;list 使用带头双向循环链表的数据结构来组织元素&#xff0c;因此list插入删除的效率非常高。 list的使用 list的构造函数 list迭代器 list的成员函…

深度神经网络(DNN)

通过5个条件判定一件事情是否会发生&#xff0c;5个条件对这件事情是否发生的影响力不同&#xff0c;计算每个条件对这件事情发生的影响力多大&#xff0c;写一个深度神经网络&#xff08;DNN&#xff09;模型程序,最后打印5个条件分别的影响力。 示例 在深度神经网络&#xf…

Matlab新手快速上手2(粒子群算法)

本文根据一个较为简单的粒子群算法框架详细分析粒子群算法的实现过程&#xff0c;对matlab新手友好&#xff0c;源码在文末给出。 粒子群算法简介 粒子群算法&#xff08;Particle Swarm Optimization&#xff0c;PSO&#xff09;是一种群体智能优化算法&#xff0c;灵感来源于…

目标检测YOLO数据集的三种格式及转换

目标检测YOLO数据集的三种格式 在目标检测领域&#xff0c;YOLO&#xff08;You Only Look Once&#xff09;算法是一个流行的选择。为了训练和测试YOLO模型&#xff0c;需要将数据集格式化为YOLO可以识别的格式。以下是三种常见的YOLO数据集格式及其特点和转换方法。 1. YOL…

计算机系统结构(二) (万字长文建议收藏)

计算机系统结构 (二) 本文首发于个人博客网站&#xff1a;http://www.blog.lekshome.top/由于CSDN并不是本人主要的内容输出平台&#xff0c;所以大多数博客直接由md文档导入且缺少审查和维护&#xff0c;如果存在图片或其他格式错误可以前往上述网站进行查看CSDN留言不一定能够…

大话设计模式-里氏代换原则

里氏代换原则&#xff08;Liskov Substitution Principle&#xff0c;LSP&#xff09; 概念 里氏代换原则是面向对象设计的基本原则之一&#xff0c;由美国计算机科学家芭芭拉利斯科夫&#xff08;Barbara Liskov&#xff09;提出。这个原则定义了子类型之间的关系&#xff0…

【人工智能基础】经典逻辑与归结原理

本章节的大部分内容与离散数学的命题、谓词两章重合。 假言推理的合式公式形式 R,R→P⇒PR,R∨P⇒P 链式推理 R→P,P→Q⇒R→QR∨P,P∨Q⇒R∨Q 互补文字&#xff1a;P和P 亲本子句&#xff1a;含有互补文字的子句 R∨P,P∨Q为亲本子句 注意&#xff1a; 必须化成析取范式…