第一章 走入并行世界
1.2 你必须知道的几个概念
1.2.1 同步与异步
1.2.2 并发(Concurrency)与并行(Parallelism)
- 并发偏重于多个任务交替执行,而多个任务之间有可能存在还是串行的
- 并行是真正意义上的同时执行
- 如果只有一个cpu是不可能真实并行的。
1.2.3 临界区
共享资源
1.2.4 阻塞(Blocking)与非阻塞(Non-Blocking)
1.2.5 死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)
都属于多线程活跃问题
- 死锁:最糟糕的一种情况
- 饥饿是指某一个或多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。如优先级太低。饥饿还是有可能在未来的一段时间内解决的。
- 活锁:秉承着“谦让”的原则,主动将资源释放给他人使用,那么就会出现资源不断在两个线程中跳动,而没有一个线程可以同时获得所有资源而正常执行。
1.3 并发级别
1.3.1 阻塞(Blocking)
synchronized或重入锁实现。
1.3.2 无饥饿(Starvation-Free)
锁是公平的,满足先来后到。即没有优先级的概念,从而避免低优先级等待导致饥饿
1.3.3 无障碍(Obstruction-Free)
最弱的非阻塞调度,可以同时进入临界区,如果出现数据修改出问题,一旦检测到,它会立即对自己所做的修改回滚,确保数据安全。是一种乐观的策略,它认为多个线程之间很有可能不会发生冲突
1.3.4 无锁(Lock-Free)
- 无锁的并行都是无障碍的。无锁并发保证必然有一个线程可以在有限步内完成操作离开临界区。
- 一个典型的特点就是包含一个无限循环,线程会不断的尝试修改共享变量,如果运气不好,一直失败,则会出现类似饥饿的现象。
1.3.5 无等待(Waite-Free)
- 要求所有线程必须在有限步内完成操作离开临界区,这样便不会出现饥饿问题。如果限制这个步骤上限,还可以进一步分解为有界无等待和线程数无关的无等待几种,它们之间的区别只是对循环次数的限制不同。
- 一种典型的无等待结构就是RCU(Read-Copy-Update),读数据读不加控制,即无等待,对写数据,先取得原数据副本,再修改副本,修改完成后,在合适的时机回写数据。
1.4 有关并行的两个重要定律
衡量串行程序改造并行程序,性能提高度。
1.4.1 Amdahl
- 定义了串行系统并行化后的加速比的计算公式和理论上限
- 加速比定义:加速比 = 优化前系统耗时 / 优化后系统耗时
- Amdahl公式推导过程:n表示处理器个数,T表示时间,T1表示优化前耗时,也就是1个处理器耗时,Tn表示使用n个处理器优化后的耗时。F是程序中只能串行执行的比例。
- Tn = T1(F + 1/n(1-1/F))
- 加速比=T1/Tn=1/(F + 1/n(1-F))
- 根据公式,如果cpu处理器数趋于无穷,那么加速比与系统的串行率成反比,如系统中必须有50%的代码串行执行,那么系统最大的加速比为2;
- 由此可见为了提高系统的速度,仅增加cpu处理器的数量并不一定能起到有效的作用。
1.4.2 Gustafson
- 从另一个角度说明处理器个数、串行比例和加速比之间的关系
- 执行之间:串行时间a + 并行时间b
- 总执行时间:a + n(处理器个数)b
- 加速比 = (a + nb)/(a + b)
- 定义串行比例 F = a/(a + b)
- 则加速比
S(n) = (a + nb)/(a + b)
= a /(a + b) + nb/(a + b)
= F+n((a+b-a)/a +b)
= F + n(1 - a/(a + b))
= F+n(1-F)
= F + n - nF
= n-F(n - 1)
- 根据公式,如果串行比例很小,并行比例很大,那么加速比就等于处理器个数,因此只要不断增加处理器个数,就能获得更快的速度
1.4.3 Amdahl定律和Gustafson定律是否相互矛盾
对同一个客观事实从不同角度去审视后的结果。
- Amdahl强调:当串行比例一定时,加速比是有上限的,不管你堆叠多少个cpu参与计算,都不能突破这个上限
- Gustafson强调:如果可被并行化的代码所占比重足够多,那么加速比就能随着cpu数量线性增长
1.5 回到Java:JMM(Java内存模型)
JMM的关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的。
1.5.1 原子性(Atomicity)
- 是指一个操作是不可中断的。一个操作一旦开始,就不会被其他线程干扰。
- 32位系统,long的读写都不是原子性的,线程之间会互相干扰
1.5.2 可见性(Visibility)
- 是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。
- 缓存优化、硬件优化、指令重排、编辑器优化,都有可能导致一个线程修改不会立即被其他线程察觉。
1.5.3 有序性(Ordering)
- 指令重排,导致了指令与原指令的顺序未必一致。
- 指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。
1.5.4 哪些指令不能重排:Happen-Before规则
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile变量的写,先发生于读
- 锁规则:解锁必然发生在随后的加锁前
- 传递性:A先于B,B先于C,A必然先于C
- 线程的start方法先于它的每一个动作
- 线程的所有操作先于线程的终结(Thread.join())
- 线程的中断(interrupt())先于被中断线程的代码
- 对象的构造函数执行、结束先于finalize()方法