【Notes4】并发


1.线程/协程/异步:并发对应硬件资源是cpu,线程是操作系统如何利用cpu资源的一种抽象。

线程想提高效率和io密切相关,程序往往都含有io。CPU上下文切换就是先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

线程是cpu调度的最小单位,左边单核cpu,3个线程(任务都是读取文件)交叉运行完:1.DMA过程中cpu一段时间不被线程阻塞。2.DMA进行数据读取时可复用,因为cpu的总线程具有多条线路,所以DMA可充分利用这些线路,实现并行读取这些文件。通过以上两点大大提高了cpu利用率,因而线程想提高效率和io密切相关。
在这里插入图片描述
如下是io(分为磁盘io和网络io)过程,io是非常耗时,比正常执行其他程序更耗时。
在这里插入图片描述
在这里插入图片描述
线程是操作系统级别概念,程序中想要开辟多线程需调用系统底层API才能进行多线程的开辟,在多线程开辟过程中浪费时间,并且在线程运行中上下文切换部分(左边切换多次,右边切换三次)有用户态和内核态转换耗时,所以一部分效率浪费在cpu切换时间点上。所以服务端连接的客户端不活跃多(即io次数少),考虑单线程(io多路复用或nio)协程。上面的1,2,3线程都有io,所以多线程效率高。

协程:协程是编程语言级别的线程,底层协程不是线程并不告诉操作系统我是新开辟的线程,因而全程处于用户态即协程是用户自定义的线程,协程可大量开辟不用担心用户态和内核态转换,一台机器上几百上千线程可能是极限,但用go语言开辟协程随便几千万,协程有异步(多去多回)的性能,同步(一去一回)的编程方式
在这里插入图片描述

2.并发(线程)之可见性:volatile

如下打印出a为false,但是程序一直没结束即另一个线程没结束。while(a)中a一直为true,因为是空语句。一个线程对a写了false,但是对另一个线程并不可见。
在这里插入图片描述
在这里插入图片描述
如下第一个core为主线程,第二个core为开辟的线程。
在这里插入图片描述
在这里插入图片描述
如上线程2不能立即读到线程1写后的最新变量值(线程1写,线程2读),多线程不可见性。如何解决多线程不可见性:加volatile关键字使a在主存和localcache间强制刷新一致。
在这里插入图片描述

3.并发之原子性(读写原子):AtomicInteger

如果线程1和2都进行基于读的变量再对读的变量再进行写,最典型操作i++,T1和T2都进行i++操作。
在这里插入图片描述
一开始i=0,经过两个线程两次i++操作结果变成了1,这显然是不对的,并且这种情况下不能用volatile保证这样操作的正确性(两个线程既有读操作,又有基于读操作的写操作,可见性只保证一个线程写另一个线程读是正确的,这里可见性不适用)。
在这里插入图片描述
现在想做的是将读操作和写操作合为一步,要么同时发生要么同时不发生(原子性)。在保证原子性同时一定以保证可见性为前提(不是并列关系,AtomicInteger类里本质上就是volatile),本身不可见的话没办法保证原子性。
在这里插入图片描述
也可用synchronized同步关键字来保证原子性发生,同步关键字同一时间只有一个线程进入代码段。
在这里插入图片描述
volatile可见性关键字最轻量级(保证一个线程写,一个线程读能读到最新的值),AtomicInteger(保证既有读操作又有写操作如i++这种场景下能保证操作的原子性)基于volatile,synchronized最重量级(能保证整个代码块中所有操作都是原子性的)。多线程情况下需要自增请使用Atomicxxx类来实现

4.并发与锁:Blocked,协程

关于内存见文章:https://blog.csdn.net/weixin_43435675/article/details/109984747
在这里插入图片描述
内存,cpu(由控制器和运算器组成,通过总线与其他设备连接),io是编程中三个最重要的点。南桥(桥就是连接)连接带宽要求低的设备如是一些鼠标键盘硬盘usb设备等。桥负责带宽比较的设备如pcie显卡,pcie硬盘,内存这些都需要高速访问。
在这里插入图片描述
如下是cpu常见参数,8核16线程(超线程)
在这里插入图片描述
系统架构指的是处理器指令集,如下常见的6种指令集:X86_64基于X86研发出,X86是32位。
在这里插入图片描述
如下是cpu状态查看
在这里插入图片描述
如何利用cpu资源?外部资源利用都是通过操作系统os提供的接口,os给了我们两种抽象即进程和线程。进程是系统资源分配,调度和管理的最小单位,比如去任务管理器查看使用内存时是看的哪个进程或哪个程序使用了多少内存而不是哪个线程,如果是哪个线程根本不知道是哪个程序里的线程,没法管理。一个进程的内存空间是一套完整的虚拟内存地址空间,这个进程中所有线程都共享这一套地址空间

线程是cpu运算的最小执行单位,也就是说真正去让cpu执行任务时,cpu看到的就是线程了,cpu并不在意是哪个进程,cpu就是轮换着线程来运行并不需要知道这个线程是属于哪个进程的

如下是线程的5种状态,只有运行中是占用cpu资源的。阻塞在java中:有线程sleep就让出cpu资源,还有synchronized关键字对应obj.wait,还有conditation对应await,还有线程的join等等方法。其中wait和await会释放锁,sleep不释放锁。
在这里插入图片描述
线程在执行时有一定性能损耗,这些损耗来自线程的创建销毁和切换,线程本质向cpu申请计算资源,用户态转内核态。os给我们提供的去获取cpu资源形式就是线程,多线程面临多线程切换问题(协程解决)。
在这里插入图片描述
协程是用户自定义线程但与os的线程不同,协程不进入内核态。自己创建一套API,协程利用线程资源。
在这里插入图片描述

4.1 synchronized:mutex重型锁,锁状态

在这里插入图片描述
jdk1.6前主要锁的工作方式都是重量级锁(平时理解的锁的方式),它的实现原理就是使用内核中的互斥量mutex,线程1拿了,线程2就拿不到了,这是内核保证的。带来另一个问题,要进入内核态,用户态内核态切换的开销大。对于重型锁的主要封装都是在C语言的pthread库中,要进入内核态,所以叫重量级锁
在这里插入图片描述
如何优化下不进入内核态,或在用户态实现出一把锁?锁状态:0是锁未被使用。。。如下左边while(state==1)是比较和赋值这两步操作不是原子性的,也就是在进行比较时两个线程同时进行了比较,发现state都=0,都执行了state=1这步操作,两个线程都认为自己拿到了锁,这就是并发问题。如何避免并发问题呢?软件层面无法保证判断和赋值两者的原子性,所以计算机提供了原语cas(compare and swap),两步操作合起来是原子性操作。

while死循环+cas形成的锁就是自旋锁。自旋锁是在用户态实现出一把锁,不进入内核态。包括cas也不进入内核态(cas计算机指令和用户态内核态没关系,还是在用户态里)。cas里有三个参数,比较state是否为0,为0的话赋值为1并且返回true,不为0的话直接返回false。也就是cas比较state是不是未被使用,是的话改为被当前线程使用并返回true,无限次尝试拿锁直到拿成功。

自旋锁spinlock优点:不进入内核态减少消耗,缺点:无限次尝试拿锁需要消耗cpu,这个缺点在竞争激烈时如很多线程一直拿不到锁,一直在死循环。所以不怎么发生竞争时,自旋锁是比较好的解决方案。
在这里插入图片描述
如上两种锁各有优劣,能不能把如上这两种锁结合呢?这种结合也就是jdk1.4之后引入的一种结合,jdk1.4后对重型锁改进引入自旋。比如用synchronized进行拿/加锁时,自旋n次都没能拿到锁,就使用mutex这种重型锁大多场景下如果竞争不激烈的话,自旋n次一定能拿到锁的,这样的话不需要进入内核态,所以效率高。

少数情况下竞争激烈,一直自旋导致cpu一直空转,所以自旋一定次数后用mutex进行加锁,mutex如果加锁失败了,线程会被挂起进入阻塞状态让出cpu资源,这样的话算是对资源的合理利用,两者进行了结合。

jdk1.4后自旋n次可通过参数-XX:PreBlockSpin设定,即进入阻塞态之前自旋多少次,没有设置的话默认10次。在jdk1.6后参数不需要我们设置了,而是给了自适应自旋。包括mutex,包括jdk1.4使用的自旋+mutex,包括jdk1.6中使用的自适应自旋+mutex,它们都是指的重量级锁

jdk1.6提出自适应自旋同时也提出了synchronized关键字应该是四种锁类型,一种叫无锁,一段时间后程序进行有锁并且是偏向锁状态(偏向于某一个线程,如果另一个线程尝试获取锁的话,锁就会升级为轻量级锁),轻量级锁主要应对多线程拿锁,但多线程间没有竞争,如果线程间一旦发生了竞争,轻量级锁会升级为重量级锁,重量级锁里面有自旋和mutex,前面讲的都是重量级锁。
在这里插入图片描述
synchronized几种锁状态,不得不提java的对象组成如下:想象成:堆+方法区
在这里插入图片描述
在这里插入图片描述

5.如何应对并发:cdn

在这里插入图片描述
在这里插入图片描述
B站/知乎/微信公众号:码农编程录
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43435675/article/details/110099272
今日推荐