并发-并发挑战及底层实现原理笔记

news/2024/5/10 21:26:41/文章来源:https://blog.csdn.net/weixin_39795049/article/details/132382586

并发编程挑战

上下文切换

  • cpu通过给每个线程分配cpu时间片实现多线程执行,时间片是cpu分配给各个线程的时间,cpu通过不断切换线程执行。
  • 线程有创建和上下文切换的开销。
  • 减少上下文切换的方方法
    – 无锁并发编程,eg:将数据的id按照hash算法取模分段,不同线程处理不同段的数据
    – cas算法:java的atomic包使用cas算法来更新数据,不需要加锁
    – 使用最少的线程:
    – 协程:在单线程里实现多任务的调度,并在单线程里维持了多个任务间的切换

死锁

避免死锁的方法

  • 避免一个线程同时获取多个锁
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
  • 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则出现解锁失败的情况

资源限制的挑战

资源限制

在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源

引起的问题

串行代码变成并发执行时,如果受限于资源,仍然串行执行,不仅不会快反而会更慢,增加了上下文切换和资源调度的时间。
例如:使用多线程在办公网并发地下载和处理数据时,导致cpu利用率达到100%,几个小时都不能运行完成任务,后来修改成单线程,一个小时就执行完成了。

解决方法

  • 硬件资源:考虑集群并行执行程序
  • 软件资源:使用资源池将资源复用

资源限制情况下进行并发编程

将不同的资源限制调整程序的并发度

并发机制的底层实现原理

  • java代码编译后会编程java字节码
  • 字节码被类加载器加载到jvm里,jvm执行字节码,
  • 最终转为汇编指令在cpu上执行

volatile应用

  • 轻量级
  • 保证共享变量的可见性
    • 可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

实现原理

如果一个字段被声明成了volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。

cpu术语
  • 内存屏障:实现对内存操作的顺序限制
  • 缓冲行:缓存中可以分配的最小存储单位
  • 原子操作:不可中断的一个或一系列操作
  • 缓存行填充:当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个缓存行到适当的缓存
  • 缓存命中
  • 写命中
  • 写缺失

Java代码

instance = new Singleton() ;//instance是volatile变量

转成汇编代码

0x01a3de1d:movb $0x0,0x1104800(%esi);0x01a3de24:lock add1 $0x0,(%esp);

Lock前缀的指令在多核处理器引发两件事情

  • 将当前处理器缓存行的数据写回到系统内存
  • 写回内存的操作会使其他cpu里缓存了该内存地址的数据无效

过程

  • 处理器先将系统内存的数据读到内存缓存后再操作
  • 对声明了volatile的变量进行写操作,jvm会向处理器发送一条lock前缀指令,将这个变量所在缓存行的数据协会到系统内存
  • 多处理器下,实现缓存一致性,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了
  • 处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器缓存行设置成无效状态,当对数据修改操作会重新从内存中把数据读到处理器缓存中。

volatile两条实现原则

  • 缓存锁定:Lock前缀指令会引起处理器缓存会写到内存,Lock#信号一般不锁总线,锁内存。缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存无效:处理器使用嗅探技术保证他的内存缓存,系统内存和其他处理器的缓存数据在总线上保持一致。
使用优化

jdk7 并发包中新增了一个队列集合类Linked-TransferQueue,在使用volatile时,用一种追加字节的方式来优化队列出队和入队的性能。

  • LinkedTransferQueue结构

    • 内部类型定义队列的头节点和尾节点
    • 内部类PaddedAtomicReference
  • 追加字节能优化队列出队和入队性能:内部类PaddedAtomicReference相对于父类AtomicReference多了将共享变量追加到64字节(15个变量+父类valule变量)

  • 为什么提高并发编程效率:有些处理器的l1,l2或l3缓存的高速缓存行使64字节宽。追加到64字节填满高速缓冲区的缓存行,避免了头结点和为节点加载到同一个缓存行,使头尾节点在修改时不会互相锁定。

  • 不能使用追加64场景

    • 缓存行非64字节的处理器:eg:P6系列和奔腾处理器,是32字节
    • 共享变量不会被频繁地写

synchronized

synchronized实现同步的基础:Java中的每个对象都可以作为锁

  • 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前类的class对象
  • 同步方法块,锁是synchronized括号里配置的对象

Monitor指令

  • monitorenter指令是在编译后插入到同步代码块的开始位置
  • monitorexit是插入到方法结束处和异常处
  • 任何对象都有一个monitor与之关联,当且一个monitor被持有后,处于锁定状态。
  • 线程执行到monitorenter指令时,将会尝试获取对象对应的monitor所有权,尝试获取对象的锁。

java对象头

synchronized用的锁是存在java对象头里的,如果对象是数组类型,虚拟机用3个字宽存储对象头,如果对象是非数组类型,用2字宽存储对象头。

  • Mark World:存储对象的hashCode,分代年龄和锁标记位

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oshe1Pci-1692516244748)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230820120519714.png)]

  • Class Metadata Address:存储到对象类型数据的指针

  • Array length:数组的长度(如果当前对象是数组)

锁的升级与对比

  • 锁状态:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态
  • 锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率
偏向锁

多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。

**偏向锁撤销:**等到竞争出现才释放锁的机制,当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

  • 无锁:thread1 访问同步块,检查对象头中是否存储了thread1,没有,cas方法替换mark word
  • 偏向锁:成功。将对象头mark word中的线程id指向自己,执行同步体
  • 偏向锁:同时 thread2访问同步块,检查对象头是否存储了thread2,没有,cas替换mark word,不成功,撤销偏向锁
  • 偏向锁撤销:thread1暂停线程,解锁,将线程id设为空,恢复线程

关闭偏向锁

java6和java7中默认启用,但在应用程序启动几秒之后才激活

关闭延迟:-XX:BiasedLockingStartupDelay=0

关闭偏向锁:-XX:-UseBiasedLocking=false

轻量级锁

加锁

  • 线程执行同步块之前,jvm先在当前线程的栈帧中创建用于存储锁记录的空间
  • 将对象头中的mark word复制到锁记录中,
  • 线程尝试使用cas将对象头中的mark word替换为指向锁记录的指针
    • 成功:当前线程获取锁
    • 失败:其他线程竞争锁,当前线程尝试自旋获取锁

解锁

  • cas将displaced mark word 替换回对象头
    • 成功:表示没有竞争发生
    • 失败:当前锁存在竞争,膨胀成重量级锁

轻量级锁膨胀流程

  • 无锁:thread1 和thread2 访问同步块,分配空间并复制mark word 到栈。
  • 轻量级锁:thread1 和thread2 进行cas修改mark word
    • thread1 成功,将mark word替换为轻量级锁,执行同步体
    • thread2 失败,因为thread1获取了锁,thread3自选获取锁
  • 升级为重量级锁:
    • thread2 锁膨胀,修改为重量级锁,线程阻塞
    • thread1 cas替换mark word 失败,因为thread2在争夺锁,thread1释放锁并唤醒等待的线程。
    • thread2线程被唤醒,重新争夺锁访问同步块

当锁处于重量级,其他线程试图获取锁时,会被阻塞,当持有锁的线程释放锁后会唤醒这些线程,被唤醒的线程会进行新一轮的夺锁之争。

优缺点对比

  • 偏向锁
    • 优点:加锁解锁不需额外消耗
    • 缺点:如果线程间存在锁竞争,会带来额外撤销的消耗
    • 场景:适用于只有一个线程访问同步块的场景
  • 轻量级锁
    • 优点:竞争线程不会阻塞,提高响应速度
    • 缺点:如果始终得不到锁竞争的线程,使用自旋会消耗cpu
    • 场景:追求响应时间,同步块执行速度非常快
  • 重量级锁
    • 优点:线程竞争不使用自旋转,不消化cpu
    • 缺点:线程阻塞,响应时间缓慢
    • 场景:追求吞吐量,同步执行速度较长

原子操作的实现原理

术语

  • 缓存行:缓存的最小操作单位
  • 比较并替换(cas):比较旧值是否发生变化,交换成新值,否则不交换
  • cpu流水线:一条x86指令分成56步后有56个不同电路单元分别执行,实现在一个cpu时钟周期完成一条指令。
  • 内存顺序冲突:假共享引起,假共享指多个cpu同时修改同一个缓存行的不同部分引起其中一个cpu的操作无效,当出现这个内存顺序冲突时,cpu需清空流水线

处理器如何实现原子操作

基于对缓存加锁或总线加锁方式实现多处理器之间的原子操作

使用总线锁保证原子性

使用处理器提供的Lock#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。

总线锁把cpu和内存之间的通信锁住了,其他处理器不能操作其他内存地址的数据,总线锁定的开销比较大。

使用缓存锁保证原子性
  • 频繁使用的内存会缓存在处理器L1,L2和L3高速缓存里。
  • 缓存锁定:内存区域如果被缓存在处理器的缓存行中,并且在Lock操作锁定期间被锁定,那么当他执行锁操作会写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性。
  • 缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行数据时,会使缓存行无效。
  • 不能使用缓存锁定的情况
    • 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定
    • 有些处理器不支持缓存锁定,对于Intel486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。

java实现原子操作

使用cas实现原子操作
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;public class Counter {private AtomicInteger atomicInteger = new AtomicInteger(0);private int i = 0;public static void main(String[] args) {final Counter cas = new Counter();List<Thread> ts = new ArrayList<Thread>(600);long start = System.currentTimeMillis();for (int j = 0; j < 100; j++) {Thread thread = new Thread(new Runnable() {public void run() {for (int i = 0; i < 10000; i++) {cas.count();cas.safeCount();}}});ts.add(thread);}for (Thread t : ts) {t.start();}//等待所有线程执行完成for (Thread t : ts) {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(cas.i);System.out.println(cas.atomicInteger.get());System.out.println(System.currentTimeMillis()-start);}private void safeCount(){for (;;){int i = atomicInteger.get();boolean b = atomicInteger.compareAndSet(i, ++i);if(b){break;}}}private void count(){i++;}
}

执行结果:
在这里插入图片描述

java1.5,jdk提供了一些类支持原子操作:AtomicBoolean,AtomicInteger和AtomicLong

cas三大问题

java并发包中有些并发框架使用了自旋cas方式实现原子操作。

  • ABA
    • 问题:旧值由A变成B再变成A,最终值没变,但实际发生了变化
    • 解决:变量前增加版本号
    • Atomic包中AtomicStampedReference解决ABA问题
      • 先检查当前引用是否等于预期引用,
      • 检查当前标志是否等于预期标志
      • 全部相等更新值
  • 循环时间长开销大:自旋cas长时间不成功,cpu带来非常大的执行开销
    • jvm支持处理器提供的pause指令,效率提升作用
      • 可以延迟流水线执行指令,是cpu不会消耗过多的执行资源,延迟时间取决于具体实现版本
      • 避免在退出循环时因内存顺序冲突,引起cpu流水线被清空,提高执行效率
  • 只能保证一个共享变量原子操作
    • 对多个共享变量操作时,不能保证原子性,可以用锁,或者多个共享变量合成一个共享变量。
    • AtomicReference类保证引用对象之间的原子性,可以把多个变量放到一个对象里进行cas操作
使用锁机制实现原子操作
  • 锁机制保证了只有获得锁的线程能操作锁定的内存区域
  • 偏向锁,轻量锁,互斥锁,除了偏向锁,jvm实现锁的方式都用了循环cas
    • 当一个线程想进入同步块时,使用循环cas的方式来获取锁,当退出同步块的时候使用循环cas释放锁。

参考:Java并发编程的艺术

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

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

相关文章

会声会影和Pr哪个好?

会声会影是加拿大Corel公司制作的一款功能强大的视频编辑软件&#xff0c;正版英文名&#xff1a;Corel VideoStudio&#xff0c;具有图像抓取和编修功能&#xff0c;可以抓取&#xff0c;转换MV、DV、V8、TV和实时记录抓取画面文件&#xff0c;并提供有超过100 多种的编制功能…

[保研/考研机试] KY11 二叉树遍历 清华大学复试上机题 C++实现

题目链接&#xff1a; 二叉树遍历_牛客题霸_牛客网编一个程序&#xff0c;读入用户输入的一串先序遍历字符串&#xff0c;根据此字符串建立一个二叉树&#xff08;以指针方式存储&#xff09;。题目来自【牛客题霸】https://www.nowcoder.com/share/jump/43719512169254700747…

五、Spring MVC 接收请求参数以及数据回显、乱码问题

文章目录 一、Spring MVC 接收请求参数二、Spring MVC 数据回显三、SpringMVC 返回中文乱码问题 一、Spring MVC 接收请求参数 客户端或者前端通过 URL 请求传递过来的参数&#xff0c;在控制器中如何接收&#xff1f; 1、当参数和 Controller 中的方法参数一致时&#xff0c;无…

设计模式之迭代器模式(Iterator)的C++实现

1、迭代器模式的提出 在软件开发过程中&#xff0c;操作的集合对象内部结构常常变化&#xff0c;在访问这些对象元素的同时&#xff0c;也要保证对象内部的封装性。迭代器模式提供了一种利用面向对象的遍历方法来遍历对象元素。迭代器模式通过抽象一个迭代器类&#xff0c;不同…

Qt安卓开发经验技巧总结V202308

01&#xff1a;01-05 pro中引入安卓拓展模块 QT androidextras 。pro中指定安卓打包目录 ANDROID_PACKAGE_SOURCE_DIR $$PWD/android 指定引入安卓特定目录比如程序图标、变量、颜色、java代码文件、jar库文件等。 AndroidManifest.xml 每个程序唯一的一个全局配置文件&…

多地图-RRT算法规划路径

RRT算法 %% %% 初始化 mapim2bw(imread(map2.bmp)); % bmp无损压缩图像500x500,im2bw把灰度图转换成二值图像01 source[10 10]; % 起始点位置 goal[490 490]; % 目标点位置 stepsize20; % RRT每步步长 disTh20; % 直到qnearest和目标点qgaol距离小于一个阈值 maxFailedAttemp…

通过LD_PRELOAD绕过disable_functions

LD_PRELOAD LD_PRELOAD是Linux/Unix系统的一个环境变量&#xff0c;它可以影响程序的运行时的链接&#xff0c;它允许在程序运行前定义优先加载的动态链接库。通过这个环境变量&#xff0c;可以在主程序和其动态链接库的中间加载别的动态链接库&#xff0c;甚至覆盖系统的函数…

微人事 部门管理 模块 (十五)

部门管理的树展示和搜索 数据展示页是个树&#xff0c;我们一次性把数据加载出来也可以通过点一次id加载查询出来出来子部门&#xff0c;我们用一次拿到说有json数据加载出来 数据不多可以用递归&#xff0c;数据很多就用懒加载的方式 由于子部门比较深就不适合&#xff0c;权…

(win系统)MSVCP100/110/120/140.dll丢失 - 解决方案

首先我们来介绍一下什么是dll dll简称动态链接库它可以节省存储空间&#xff1a;由于DLL可以被多个程序共享&#xff0c;因此可以减少磁盘空间的使用。也能提高代码重用率&#xff1a;通过使用DLL,我们可以将一些常用的功能封装成独立的模块&#xff0c;从而提高代码的重用率。…

电脑上安装,多版本node

手上有一个vue3的项目&#xff0c;sass配置如下图所示&#xff1a; 安装了Python3.10和node 16.14.0&#xff0c;项目能正常install 跟run。 因工作需要&#xff0c;收上有一个vue2的项目&#xff0c;sass配置如下图所示&#xff1a; 执行npm intsall 的时候一直报Python2找不…

第18集丨Vue脚手架的默认配置

目录 一、查看默认配置1.1 在此系统中禁止执行脚本1.2 错误解决方案1.3 执行成功生成的配置项 二、关闭语法检查 一、查看默认配置 Vue脚手架隐藏了所有 webpack 相关的配置&#xff0c;若想查看具体的 webpak 配置&#xff0c;请执行&#xff1a;vue inspect > output.js …

nodejs使用PassThrough流进行数据传递合并

在Node.js中&#xff0c;流&#xff08;stream&#xff09;是处理数据的强大工具&#xff0c;它们允许我们以流式方式处理大量数据&#xff0c;而不必一次性将所有数据加载到内存中。PassThrough是Node.js中的一个流类型&#xff0c;它在数据流传递过程中起到 无操作 的中间层&…

22.0.6 LEADTOOLS 增加了 Python 支持 -Crack

LEADTOOLS 增加了 Python 支持 Python 开发人员现在可以利用 LEADTOOLS 技术&#xff0c;包括识别、多媒体和成像。 2023 年 7 月 18 日 - 16:40新版本 特征 添加了完整的 Python 支持 LEADTOOLS Python 支持包括高级图像处理功能、OCR、PDF、条形码识别和表单处理&#xff0c;…

OpenCV图片校正

OpenCV图片校正 背景几种校正方法1.傅里叶变换 霍夫变换 直线 角度 旋转3.四点透视 角度 旋转4.检测矩形轮廓 角度 旋转参考 背景 遇到偏的图片想要校正成水平或者垂直的。 几种校正方法 对于倾斜的图片通过矫正可以得到水平的图片。一般有如下几种基于opencv的组合方…

LLM架构自注意力机制Transformers architecture Attention is all you need

使用Transformers架构构建大型语言模型显著提高了自然语言任务的性能&#xff0c;超过了之前的RNNs&#xff0c;并导致了再生能力的爆炸。 Transformers架构的力量在于其学习句子中所有单词的相关性和上下文的能力。不仅仅是您在这里看到的&#xff0c;与它的邻居每个词相邻&…

docker的资源控制及数据管理

docker的资源控制及docker数据管理 一.docker的资源控制 1.CPU 资源控制 1.1 资源控制工具 cgroups&#xff0c;是一个非常强大的linux内核工具&#xff0c;他不仅可以限制被 namespace 隔离起来的资源&#xff0c; 还可以为资源设置权重、计算使用量、操控进程启停等等。 …

苍穹外卖 day1 搭建成功环境

引入 idea找不到打包生成的文件目录怎么办&#xff0c;首先点击这个小齿轮 show ecluded files然后就能找到隐藏的文件 这个jar包内含tomcat&#xff0c;可以直接丢在linux上用 开发环境&#xff1a;开发人员在开发阶段使用的环境&#xff0c;一般外部用户无法访问 测试环…

shell脚本之循环语句

循环语句 循环含义 将某代码段重复运行多次&#xff0c;通常有进入循环的条件和退出循环的条件 for循环语句 一般知道循环次数使用for循环 第一类 格式1&#xff1a; for名称 in 取值次数;do;done; 格式2&#xff1a; for 名称 in {取值列表} do done# 打印20次 for i i…

k8s之Pod及Probe 探针机制(健康检查机制)

文章目录 1、Pod1.1、定义1.2、Pod的形式1.2、Pod的使用1.3、 Pod生命周期1.4、生命周期钩子1.5、临时容器1.5.1、定义1.5.2、使用临时容器的步骤 1.6、静态Pod 2、Probe 探针机制&#xff08;健康检查机制&#xff09;2.1、探针分类2.2、Probe配置项2.3、编写yaml测试探针机制…

学C的第三十四天【程序环境和预处理】

相关代码gitee自取&#xff1a; C语言学习日记: 加油努力 (gitee.com) 接上期&#xff1a; 学C的第三十三天【C语言文件操作】_高高的胖子的博客-CSDN博客 1 . 程序的翻译环境和执行环境 在ANSI C(C语言标准)的任何一种实现中&#xff0c;存在两个不同的环境。 &#xff0…