(一)走入并行世界

本文深入探讨了并行和并发的区别,包括同步与异步的概念,以及并发级别的分类。讲解了临界区、阻塞与非阻塞、死锁、饥饿和活锁等多线程问题,并介绍了Amdahl定律和Gustafson定律在并行计算中的应用。此外,还详细阐述了Java内存模型(JMM)中的原子性、可见性和有序性,以及如何防止指令重排带来的问题。
摘要由CSDN通过智能技术生成

第一章 走入并行世界

一、必知的概念

1、同步和异步

同步:串行化的执行,前面A执行完成才能执行后面B的。(需要前面的执行返回结果才开始后面的操作)

异步:执行操作A的同时,去执行操作B。不需要等A操作完返回结果再去操作B。此时利用多线程执行多个操作。

2、并发和并行

并发:多个任务交替执行,利用多线程实现(多个线程间互相切换执行),非真正意义上的同步进行,而是因为线程切换快执行快从而体现的"同时执行"的效果。(一个进程内其实只能有一个线程在运行)

并行:真正的同时执行,多个进程同时执行各自的操作。(多进程需要多核cpu的环境)

3、临界区

即多线程下竞争的公共资源(共享数据),但每次只能有一个线程使用占用它。

当该资源被占用后,其他线程要使用则需要进行等待。

在并发程序中,临界区资源是需要保护的对象。

4、阻塞和非阻塞

临界区资源被占用,此时其他线程要使用该资源就需要在该临界区中等待,此时将线程挂起,这就是阻塞。

非阻塞的意思反之理解。

5、死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)

死锁、饥饿、活锁都属于多线程的活跃性问题,如果发生这几种情况,那其他线程就不再活跃(很难继续往下执行)。

死锁:线程A占用了a,线程B占用了b。此时B需要用到a就先挂起等待,而A此时需要用到b也挂起等待资源释放。就这样两个线程都一直阻塞等待另一个线程释放临界区资源。

饥饿:一个或多个线程因为某种原因无法获取所需的资源,导致一直无法执行。这种情况称为饥饿。

(例如线程优先级别太低、某个线程长时间占用资源不放等情况)

活锁:两个线程互相的礼让资源,导致资源在两个线程间跳动,而没有一个线程可以真正拿到资源正常执行。

6、并发级别

1、阻塞

一个线程如果是阻塞的,那么在其相应资源被释放前该线程无法继续执行。使用synchorized和重入锁时就能得到阻塞的线程。

2、无饥饿

对于同一个资源的分配可以是不公平的,可以实现公平锁和非公平锁。对于非公平锁来说,系统允许高优先级的线程插队,这样就可能导致低优先级线程产生饥饿。如果此时锁是公平的,此时就会根据先来后到的规则,那么就不会有因线程优先级而产生的饥饿现象了。

3、无障碍

无障碍是一种最弱的非阻塞调度。多个线程可以一起修改临界区,当数据被改坏时,对于无障碍的线程检测到这种情况,就会对自己所做的修改进行回滚,以确保数据安全。

而当多个无障碍线程同时去使用这个临界区时,都发现了冲突这个线程,此时要是所有线程都进行回滚操作,那么不就是会出现没有一个线程使用到临界区资源且正常退出。所以此时无障碍实现可以依赖一个“一致性标记”,线程操作前先读取并保留这个标记,再操作后再次读取看这个标记是否被更改过。如果被更改了则可进行重试操作。(在修改数据前都需要更新这个一致性标记,表示数据不再安全)(是不是有点像乐观锁的实现)

4、无锁

多线程对临界区进行访问时,总有一个线程能成功获取资源。其他获取失败的线程会继续重试获取直到获取成功,如果多次尝试不成功这个时候就会出现类似于饥饿的现象,从而导致线程会停止。(类似自旋锁)

5、无等待

无锁是可以允许获取到资源的线程在有限步内完成,而其他线程可以不断去重试。

而无等待即对等待重试的线程也做了限制,限制其在有限步内完成。

例如经典的一种无等待结构RCU:对读不加控制,对于要进行数据修改的线程,此时给每个线程一个数据副本,然后线程在副本上修改数据,修改完后在合适的时机回写数据。

7、有关并行的两个重要定律

1、Amdahl定律

定义了串行系统并行化后的加速比的计算公式和理论上限。

加速比定义:   加速比 = 优化前系统耗时 /  优化后系统耗时 = 1/(F +(1-F)/n)

比例越高说明优化效果更好。(其中F表示程序中只能串行执行的代码的比例;n代表处理器的核数)

由公式可得要提高系统的速度,不仅仅是增加CPU处理器的数量就可以,也要从根本上修改程序的串行行为。这样才能获取到更大的加速比。

2、Gustafson定律

Gustafson定律也试图在说明处理器个数、串行化比例和加速比之间的关系。

加速比公式:   加速比S(n)= n - F(n-1)

3、两个定律的异同

虽然两个定律推导出来的公式不同,但是这两个定律都是对同一个客观事实从不同角度去审视后的结果。(侧重点不同而已)
Amdahl强调的是当串行化比例一定时,加速比是有上限的,不管你堆积多少个cpu参与计算都不能突破这个上限。

而Gustafson关心的是如果可被串行化的代码所占比例足够大,那么加速比就会随着cpu的数量线性增长。

所以这两个定律并不矛盾。

8、JMM(Java的内存模型)

JMM的关键技术点都是围绕着多线程的原子性、可见性、有序性来建立的。因此我们需要先来了解这些概念。

1、原子性

表示操作是不可断的。即多线程中一个操作一旦开始了就不会受其他线程干扰。

2、可见性

一个线程修改了某个共享变量的值时,其他线程能立刻知道这个修改。

其中有一个ABA问题,就是一个线程1去读取一个值为A,此时去做其他操作,在做其他操作的时候,这个值被其他线程修改为B值,然后再改为A值,此时线程1再去读取这个值发现还是A值,但其实这个值已经经过了从A到B再到A的过程了。(ABA问题可以用时间戳或版本号来唯一标识,通过这个标识判断该值是否被修改过了)

3、有序性

对于一个线程来说,程序应该是从前往后依次执行的,但是由于JVM会对程序指令进行优化,从而将不影响程序结果的指令进行重排从而达到优化的效果。而在多线程的情况下,这种指令重排可能导致线程不安全。

而有序性则是防止指令重排。

 

那为什么JVM、操作系统会进行指令重排?
其实一条指令的执行可以分为很多步,简单可以分为以下几步:

  • 取指IF
  • 译码和取寄存器操作数ID
  • 执行或者有效地址计算EX
  • 存储器访问MEM
  • 写回WB

在cpu中一条汇编指令不是一步执行完的,而是需要分多个步骤依次执行的,

而且每个步骤所涉及的硬件也可能不同。例如取指时会用到PC寄存器和存储器,译码时会用到指令寄存器组,执行时会用到ALU,写回时要用到寄存器组。(ALU指算术逻辑单位。他是cpu的执行单元,是cpu核心的组成部分,主要功能是进行二进制算术运算)

由于每个步骤都可能使用不同的硬件完成,因此则发明了流水线技术来执行指令。例如:
image.png

可以看到,流水线技术即指某个硬件进行执行操作时,可以看成一条流水线,分别执行多个指令的相应的操作。例如IF操作,此时如果指令1、2需要执行,指令1先执行,此时PC寄存器和存储器先进行指令1的IF操作,然后再执行指令2的IF操作。这样依次多个指令的相应IF操作就会形成一条流水线。这样可以使得效率提高。

而流水线存在一个问题就是流水线中断:
一旦流水线中断,此时所有的硬件设备会受影响进入一个停顿期,此时要再进入流水线满载损失的性能会比较大,而指令重排1就是为了尽量的减少流水线得中断。当然指令重排只是减少中断得一种技术。

例如:
image.png

此时因为后面得指令中有一个大叉表示中断,此时因为R2得数据还没有准备好(需要R2得数据结果),导致add操作必须进行一次等待。从而导致后面得指令都慢一拍。我们再看一个例子:
image.png

此时add和sub都需要等待上一条指令得结果,所以需要插入不少得停顿。此时就可以利用指令重排尽量得消除停顿。我们此时只需要将lw re,e  和 lw rf,f 移动到前面执行即可。此时先加载e和f对程序是没有影响得。

image.png

image.png

重排后,最终结果如上,可以看到所有的停顿都消除了,此时流水线就能顺畅的执行了。(指令重排就是在执行结果不变的情况下,利用各个硬件的空闲时机去执行后面的操作。从而提高利用率和效率)

4、哪些指令不能重排?(happen-before原则)

遵循下面的原则的指令是不能重排的:

  • 程序顺序原则:一个线程内保证语义的串行性
  • volatile原则:volatile变量的写先于读发生,这保证了volatile变量的可见性(所以volatile具有可见性和有序性)
  • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
  • 传递性:A先于B,B先于C,那么A必然先于C
  • 线程的start()方法先于它的每一个动作
  • 线程的所有操作先于线程的终结(Thread.join())
  • 线程的中断(interrupt())先于被中断线程的代码
  • 对象的构造函数的执行、结束先于finalize()方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值