当前位置: 首页 > news >正文

Java中进阶并发编程

第一章、并发编程的挑战 

并发和并行:指多线程或多进程 

线程的本质:操作系统能够进行运算调度的最小单位,是进程(Process)中的实际工作单元

进程的本质:操作系统进行资源分配和调度的基本单位,是程序的一次动态执行过程。

多线程定义:在同一个进程内创建多个线程,共享进程的内存和资源(如堆、全局变量),但每个线程有独立的栈和程序计数器。

多进程定义:操作系统同时运行多个独立的进程,每个进程有独立的内存空间和系统资源。

操作系统:操作系统(Operating System, OS)是计算机系统的核心管理者硬件与软件的桥梁

并发和并行的区别是什么? 

【本文专注于多线程】

并行:不同的线程或进程间互不干扰 

并发:不同的线程之间有资源的竞争

线程的基本代码: 

Thread t1---->创建第一个线程

Thread t2----->创建第二个线程

运行时是两个任务交替运行;去掉t1.join()/t2.join(),运行是3个任务交替运行

线程的几种状态: 

操作系统的指令执行不是立刻的,而是一个任务调度的过程;指令的最终执行靠的是CPU里的运算核心。

在任务管理器中,句柄指变量,核心指CPU中间的运算核心【ALU】,每个核心同一时刻只能执行一个任务。【因为电压信号同时传输会合并为一个信号】;内核为14指CPU同时只能执行14个指令,每个基础指令是线程级别,可以认为是一个任务--->CPU同时最多执行14个任务;快速切换任务执行;

1、CPU内部的信号指令1010,都是电压信号,高电压代表1,低电压代表0,电压信号不能同时传递,必须是排队传输。

2、计算机内部的各个硬件之间的指令传输都是高低电压传输硬件之间的指令传输,由于电压传输,每个指令必须得排队。 

 就绪队列:每个任务都进入就绪态,操作系统随时可以选中去执行,由就绪态选中变成运行状态,当剩的时间片不多,执行完时,从就绪队列清除,变成死亡状态。

时间片: 决定了任务在就绪队列中能连续占用CPU的时长

在电脑中任何任务的提交不能绕过操作系统。

队列为什么不是先进先出?

 先进先出是公平队列,整体性能损耗较大;

操作系统里是非公平队列,判定时是按顺序判断该任务是否符合条件,只有符合条件的才会被选中;所以态既叫队列,又叫非公平队列;

为什么微信,QQ等一直在运行状态? 

里面有一个死循环,当用户点击关闭触发条件判定, 循环判定不满足,程序结束。

在微信、QQ等应用保持后台运行的场景中,“死循环”通常指的是主线程的事件循环(Event Loop)。

用户是不能让程序直接进入运行态的,用户只能交给操作系统让它进入就绪态。

 

代码内存图:

 方法的执行都是拷贝入栈;方法本身也是一个栈结构;Java的引用类型变量底层是C语言的指针;new的两个对象在底层是C语言创建的空间,是在堆区域创建了两个线程对象;t1对象属于线程类;Thread里有很多属性和方法,上述代码只是对run方法进行重写;

run和start方法是非静态方法,在每个对象里都有一份;

main方法被static修饰,只有一份;

方法的调用都是拷贝入栈;

t1的start方法拷贝入栈后,t1的start方法会把它的整个线程信息交给操作系统的就绪队列,提交完后出栈------t2的start拷贝入栈,t2的start把t2的信息提交给操作系统,t2在就绪队列,t2再出栈-----主方法本身也在就绪队列里----操作系统随时选中执行,选中后再创建新线程【比如针对t1创建t1线程栈,针对t2创建t2线程栈】---t1被操作系统选中后会执行里面的run方法,run方法拷贝进入t1线程;

类.方法-----方法是静态的

通过主线程可以创建子线程,创建完之后就相互独立;

先进入就绪态被操作系统选中的概率大,但是顺序不确定;

上下文切换【面试点】

CPU每次执行完任务会把任务执行到哪了记下来,下次再执行时会去读从哪接着开始执行,时间损耗是毫秒级。

定义:

上下文切换是操作系统在多任务环境中,将CPU从一个线程(或进程)切换到另一个线程(或进程)时,保存当前任务状态并恢复新任务状态的过程。它是并发编程中影响性能的关键因素之一。

时间片一般是几毫秒——几十毫秒,不同操作系统不一样。

 多线程一定会涉及到上下文切换,频繁的上下文切换一定会额外消耗CPU的时间。

什么情况下用多线程?【面试题】

多线程在CPU浪费严重的情况下使用。【执行时间和等待时间比例特别悬殊的情况相爱,采用多线程】(一定是io密集,就是大量的高频访问网落访问硬盘)

IO密集型(输入输出):出现了大量的CPU等待时间。

io:访问网落,访问硬盘

指令密集型:大量给CPU任务---单线程合适

join() 是 Java 多线程编程中的一个核心方法,用于让当前线程等待另一个线程执行完毕

t1、t2都执行完时,for循环才会执行。

t1、t2的执行互不等待;只有t1执行完时,t2才会发出join指令

 在栈里,只有栈顶的元素处于运行状态,不在栈顶处于停止运行状态。

在这个代码里,对比单线程执行两个for循环和两个线程分别执行for循环哪个更快。【thread.jon和输出时间戳互换】

串行:单线程---并发:多线程

只有一个核心,单线程最快

用什么工具来监控内存?----Lmbench3可以测量上下文切换的时长,指令:vmstat---可以测量上下文切换的次数【面试点】

如何减少上下文切换?【面试点】----无锁并发编程、CAS算法、使用最少线程和使用协程。


 


grep指令:文本搜索工具;在文件或输入流中匹配指定模式(正则表达式)

awk指令:文本处理与报表生成;逐行处理文本,按字段(默认以空格/Tab分隔)提取、计算或格式化输出。

sort指令: 文本排序工具;对文件或输入流按行排序(默认按字典序)。

dump指令:快照,查看这一瞬间内存中的信息【面试点】

指令结合案例:

sudo:Linux中使用管理员指令。

在后面的目录下查询id为31177的线程,把查看的信息保存为后面路径下的dump17文件。

 用grep指令在dump17文件中筛选行里有java.lang.Thread.State的信息,然后统计完排序。


 死锁

 非静态方法必须依托对象的存在,在每个对象里都有一份。

synchronized(A)是对A加锁;加锁只能对引用类型加锁,锁住A后其他线程不允许读A ;

当进入睡眠状态时,即使时间片没执行完也必须让出CPU,并且不在就绪队列。【面试点】

 锁住A后什么时候会释放锁?---把{ }里的全部执行完才会释放

一个线程就算进入睡眠状态,也不会释放锁。【面试点】

sleep方法:让出CPU,但不释放锁

wait方法:让出CPU,并且释放锁 【面试点】

 加锁其实是给当前线程加上标记。

t1锁住A后沉睡,t2被操作系统选中锁住B后访问A失败,t2让出CPU进入阻塞队列,但锁仍未释放;2秒之后t1进入就绪队列,想要锁B失败,t1也进入阻塞队列------->死锁;

但不会消耗CPU,CPU仍可以执行其他任务,只是这两个任务永远执行不完了

竞争失败要进入阻塞队列;进入阻塞队列操作系统不会选中;

 恰巧的情况下,t2先执行,就不会出现死锁了。

 如何避免死锁?【面试点】

 

 

第二章、JAVA并发机制的底层实现原理

synchronize:重量级锁;读写都锁住---->读写都安全

volatile:轻量级锁(功能不全的锁);只能锁住写-->读安全,写不安全【面试点】

 volatile在多处理器(CPU里的多核心)开发中保证了共享变量的“可见性”----->当一个线程修改另一个共享变量时,另外一个线程能读到这个修改的值。

CPU要从内存中读取数据进行操作;内存中存储的是变量以及函数在它的运行状态下的数据;

CPU通过总线(本质上是导线)向内存中传输指令(1010指令就是电压信号);

单根导线上同一时刻只能过一个电压信号;总线的宽度一般是36-41位(并排三十多根到四十多根的导线);

一个指令通常先传地址,再传数据;

一次指令差不多是一个最基本的指令,也就是总线的宽度;不同的指令是不同同时传输的,不然电压信号会相互干扰;

指令在总线必须排队传输--->存在指令队列

内存和CPU的数据是双向的; 但CPU发送的指令比内存多,所以指令队列在CPU;

内存同一时刻只能被一个指令所指挥;

 假设现在有三个线程,分别对a,b,c for循环一万次,a通过总线读完+1再通过总线往回更新;

当a进行读写操作时,总线一直被占用,其他两个核心没办法操作;--->此时多核没意义

现在,CPU和内部的存储一直交互释放了总线,总线就可以把其他线程的数据读过来操作了--->支持了多核CPU的发展

a加到1万返回内存,在总线上一来一回,对总线占用的不多;

所以CPU的存储越多,存的数据越多,对核心的支持也越多,对总线的压力降低越明显,CPU内部存储和性能关乎大;

长距离网络传输时,一根导线可以同时存在多个电压信号。

 

 加入中转站后效率大幅度提高,但转发也损耗时间,不是越多越好,平衡点性能最好的平衡点是三级缓存。

多级缓存(L1/L2/L3)通过 减少CPU核心的等待时间 和 优化数据局部性,显著提高核心利用率。

高速缓存的并发性问题

现在有两个线程操作同一个变量a,线程1拿到时间片,将a加到10 ,还没往回更新时时间片到期,停止运行,线程2拿到时间片,将a加到10,往回更新,任务执行完释放CPU,线程1接着执行,往回更新---->此时两个线程一共加了20,但是最终的结果是10---->导致了相互覆盖问题。

高速缓存优点:增加传输效率,提升CPU利用率

高速缓存缺点:会导致数据相互覆盖

高速缓存往回更新是随机的,不一定计算完后往回更新;------->最终结果是不确定的

高速缓存往回更新时,高速缓存内的数据会清空,下一次计算需要重新从内存读取数据。


内存屏障:在内存中有一个变量标记,根据这个标记判断是否要访问。

缓存行:高速缓存的基本存储单元【一个缓存行64字节,没一个缓存行和外界相连的导线线路进行数据交互;】处理器填写缓存线时会加载整个缓存线,需要使用多个主内存读周期【一个周期从内存中读数据叫做一个主内存周期】

计算机存储区域:内存、硬盘、CPU内部的高速缓存---数据是一份份存储的,硬盘和内存中的数据单元是4KB

计算机中最小的物理单元是一字节一个存储单元,操作系统给硬盘和内存划分的逻辑存储是4KB一个存储区域-----在硬盘上,一个存储单元是栈区,在内存中一个存储单元是一个页,在高速缓存中存储单元是一个缓存行64字节。

一个缓存行可以存多份不同任务的数据,其中一个任务占着线操作时,其他任务就不能对它操作了--->所以每个任务过来操作时,相当于独占整个缓存行(不同芯片不一样,但每个都是独占的),造成一定性能损耗。

原子操作:不可中断的一个或一系列操作(一系列操作要么都成功,要么都失败,其中一个步骤如果失败,其他步骤都要还原)

在电脑底层只存在C语言的基本类型数据(6种基本数据:short  int  long bool double char ),其他语言在电脑内部都会转成C语言;

缓存行主要用于存储六种基本类型的数据,其容量从一字节到八字节不等。由于缓存行可容纳大量数据,当多个进程同时进行读写操作时,容易造成拥堵现象。具体而言,缓存行中存储的数据越多,发生排队的概率就越大;反之,数据量越少,排队概率则越小。

为优化性能,我们可采取以下策略:首先,尽量在缓存行中存储体积较大的数据,以减少数据总量,从而降低竞争概率。其次,在存储数据时,可采用独占式填充策略,即用自身数据填满整个缓存行。这种做法的优势在于,当需要读取数据时,可以避免竞争,实现快速访问,从而显著提升性能。

然而,这种优化策略的适用性取决于数据规模。在数据量较少时,这种方法是可行的;但当数据量较大时,若缓存行中存储的数据过少,反而会导致整体性能下降。因此,缓存行的设计需要权衡利弊,这也是为什么标准缓存行大小通常设定为64字节。过大的缓存行会增加拥堵风险,反而降低读取效率,有时甚至不如直接从内存读取。

对于追求极致性能的场景,可以将整个64字节缓存行用于存储单一数据。这种独占式存储方式能够确保每次读取都畅通无阻,达到最佳性能表现。

缓存命中:高层缓存容量有限,仅能存储少量数据。当核心处理器需要访问数据时,首先会查询高速缓存。若所需数据存在于缓存中,则直接返回,这种情况称为缓存命中;若缓存中不存在,则需访问内存,这种情况称为缓存未命中。由于缓存空间有限,部分数据可能存储在缓存中,而另一部分则不在,因此每次访问都需要先检查缓存是否存在所需数据,再决定是否访问内存。这一过程就是缓存命中的基本原理。

 写命中:------>针对高速缓存

当数据写回到内存缓存区域时,系统会首先检查该缓存地址是否存在于缓存中。以读取变量a为例:首先读取a,然后对a进行操作,操作完成后再将结果更新回缓存。在这个过程中,如果缓存中存在a的地址,称为写命中;如果不存在,则称为写未命中。值得注意的是,由于高速缓存空间有限,之前存储的数据可能已被清理。如果数据已被清除,在写回时就无法找到对应的缓存地址,从而导致写未命中。

写缺失: -------->针对内存

举例:a=5往回更新时,可能有其他线程把内存中的a删除,内存中的a没了,这种情况就是写缺失。

 【以上术语只有原子操作、缓存命中是学术性术语,其他几个面试面不到】


x86处理器是指CPU的内部结构;以下内容以x86处理器为主:

主流CPU处理器: x86架构、ARM架构【军队政府学校】、RISC-V 架构

 new的对象被volatile修饰,转换成汇编代码,里面有lock指令;

有volatile变量修饰的共享变量进行写操作时会多出第二行汇编代码---->CPU架构

 

当内存中的a被volatile变量修饰时,其中一个a往回更新时,另一个在高速缓存中的a失效 ;

但是用volatile修饰后还是会出错,如图,当a=10进入队列时,时间片到期,a=20执行指令,高速缓存中的数据失效,但问题是数据已经进入队列,所以仍会出现覆盖问题。

volatile并不能保证准确性。

补充:内存图角度理解并发编程

进程:正在执行的程序,内存为正在执行的程序开辟内存空间

线程:程序的执行过程,线程的执行依赖方法的不断入栈出栈

内存会为正在运行的程序开辟内存空间;

方法区:存储类信息

对象在堆里开辟空间

只有争抢的资源需要加锁,不争抢不用加锁

synchronized不能对变量进行加锁

锁是要加在被争抢的资源上的

一个完整的操作不能分开加锁

synchronized修饰静态方法:锁定的是非静态方法:锁方法的调用者,修饰代码块:锁定传入对象

相关文章:

  • langchain4j中使用milvus向量数据库做RAG增加索引
  • 新能源汽车电池加热技术:传统膜加热 vs. 脉冲自加热
  • C++类成员
  • 【技巧】使用frpc点对点安全地内网穿透访问ollama服务
  • Ascend的aclgraph(五)PrimTorch TorchInductor
  • 网页Web端无人机直播RTSP视频流,无需服务器转码,延迟300毫秒
  • Dagster Pipes系列-1:调用外部Python脚本
  • 按钮导航组件 | 纯血鸿蒙组件库AUI
  • 基于STM32、HAL库的DPS368XTSA1气压传感器 驱动程序设计
  • Java高频面试之并发编程-16
  • 设置环境变量启动jar报
  • 基于SpringBoot的蜗牛兼职网设计与实现|源码+数据库+开发说明文档
  • Qt Creator 配置 Android 编译环境
  • 火山RTC 6 自定义视频
  • 深入解析MySQL联合查询(UNION):案例与实战技巧
  • 区块链技术构建电子发票平台“税链”
  • JVM之垃圾回收器
  • 开源 RPA 工具深度解析与官网指引
  • 【Git】GitHub上传图片遇到的问题
  • Spark,序列化反序列化
  • 《三餐四季》广东篇今晚开播:食在岭南,遇见百味
  • 巴基斯坦外长:印巴已同意立即停火
  • 欧洲理事会前主席米歇尔受聘中欧国际工商学院特聘教授,上海市市长龚正会见
  • 协会:坚决支持司法机关依法打击涉象棋行业的违法行为
  • 股价两天涨超30%,中航成飞:不存在应披露而未披露的重大事项
  • 马上评|比余华与史铁生的友情更动人的是什么