基本概念
- 并发(concurrency)和并行(parallelism)
并发偏重多个任务交替执行,而多个任务之间可能还是串行的。
并行是真正意义上的“同时执行”。 - 临界区
临界区用来表示一种公共资源,或者说是共享数据,可以被多个线程使用。但是每次只能由一个线程使用它。 - 阻塞(blocking)和非阻塞(non-blocking)
阻塞指多个线程因为需要某个临界区资源而发生等待。
非阻塞是指没有一个线程会妨碍到其他线程执行。 - 死锁(deadlock)、饥饿(starvation)和活锁(livelock)
死锁是最糟糕的一种情况,线程之间需要彼此的资源,但都抢占资源不释放,造成阻塞状态永久维持下去。
饥饿是指因为线程优先级太低,高优先级的线程不断抢占它需要的资源,导致该线程长久等待。
活锁是指两个线程发现自己的资源不够之后,都释放手上的资源,导致资源在两个线程之间不断跳动,而没有一个线程能够正常执行。
- 并发(concurrency)和并行(parallelism)
并发级别
- 阻塞(blocking)一个线程是阻塞的,那么其它的线程释放资源之前,当前线程无法继续执行。使用synchronized或者重入锁会使线程这是。
- 无饥饿(starvation-free):对于非公平的锁来说,系统允许高优先级的线程插队,会造成饥饿;而公平的锁则不会造成饥饿。
- 无障碍(obstruction-free)是一种最弱的非阻塞调度。如果两个线程无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。但如果大家一起修改了临界区,那么无阻碍线程就会对自己的修改进行回滚。如果临界区中有严重的冲突,会导致所有线程会不断回滚自己的操作,从而没有一个线程能够走出临界区。
- 无锁(lock-free)的并行是无障碍的,所有的线程都能尝试对临界区进行访问。但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。运气不好的线程还是会饥饿。
- 无等待(wait-free)在无锁的基础上更进一步进行扩展,它要求所有的线程丢必须在有限步内完成,这样就不会引起饥饿问题。一种典型的无等待结构是RCU(Read-Copy-Update)。它的基本思想是,对数据的读可以不加控制,因此所有的读线程都是无等待的。写数据时,先取得原数据的副本,然后只修改副本,并在合适的时候写回。
有关并行的两个重要定律
- Amdahl定律,它定义了串行系统并行化后的加速比的计算公式和理论上限。
- 加速比 = 优化前系统耗时 / 优化后系统耗时。 加速比越高,表明优化效果越明显。
- 使用多核CPU对系统进行优化,优化的效果取决于CPU的数量以及系统中的串行化程序的比重。
- 加速比 = 1F+1n(1−F) 1 F + 1 n ( 1 − F ) ,其中n是处理器个数、F是串行比例
- 加速比的极限是 1F 1 F
- Gustafson定律,它也试图说明处理器个数、串行比例和加速比之间的关系。
- 加速比= n−F(n−1) n − F ( n − 1 )
- 只要不断增加处理器,就能够获得更快的速度。
- Amdahl定律与Gustafson定律是否矛盾
- Amdahl强调:当串行比例一定时,加速比是有上限的,不管你堆叠多少个CPU参与计算,都不能突破这个上限。
- Gustafson强调:如果可被并行化的代码所占比例足够多,那么加速比就能随着CPU的属性线性增长。(线性增长是否有问题?)
- Amdahl定律,它定义了串行系统并行化后的加速比的计算公式和理论上限。
JMM
原子性(atomicity)是指一个操作是不可中断的。
- 对于32位系统来说,对long类型数据的读写是不具有原子性的,因为long类型占64位。
可见性(visibility)是指当一个线程修改了某一个共享变量时,其他线程能够立马知道这个修改。
- 当CPU1和CPU2上各有一个线程,它们共享变量t,缓存优化的原因,CPU1将t缓存到cache中,此时如果CPU2对该变量进行改动,那么CPU1无法意识到这个改动。当然还有编译优化、硬件优化、指令重排等技术也会导致这个现象。
有序性(ordering),在并发时,程序的执行可能会出现乱序。
- 有序性的问题的原因是:程序在执行时,可能会进行指令重排。
- 执行重排的基本前提是,保证串行语义的一致性,但是不保证多线程间的语义一致。
- 指令重排的意义:在流水线技术中,每次指令中断后,需要几个周期才能够满载,因此性能损失比较大。为了提升性能,就要减少指令被中断的次数。而指令重排就是为了减少指令被中断的次数。
Happen-Before原则:指令重排不可违背的原则
- 程序顺序原则:一个线程内保证语义的串行性。
- volatile规则:volatile变量的写,先发生于读,这保证了volatile的可见性。
- 锁规则:解锁必然发生于随后的加锁之前。
- 传递性:A先于B,B先于C,那么A必然先于C。
- 线程的start()法先于它的每一个动作。
- 线程的所有操作先于线程的终结。
- 线程的中断先于被中断的线程的代码。
- 对象的构造函数的执行、结束先于finalize()方法。
实战Java高并发程序设计(一)走进并发世界
最新推荐文章于 2024-09-03 23:18:22 发布