并发编程痛苦之源
一
CPU、内存、I/O 设备都在朝着更快的方向努力,三者间有一个速度差。
根据木桶理论(一只水桶能装多少水取决于它最短的那块木板),程序整体的性能取决于最慢的操作——读写 I/O 设备。
为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献
- CPU 增加了缓存,以均衡与内存的速度差异
- 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异
- 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用
但并发程序很多问题的根源就在这里。
二
一个线程对共享变量的修改,另外一个线程能够立刻看到,称之为可见性。
也即 CPU 缓存与内存的数据一致性问题(见单核、多核 CPU 的缓存与内存的关系图)。
// 作者使用了下面的代码来验证多核场景下的可见性问题
public class Test {
private long count = 0;
private void add10K() {
int idx = 0;
while(idx++ < 10000) {
count += 1;
}
}
public static long calc() {
final Test test = new Test();
// 创建两个线程,执行add()操作
Thread th1 = new Thread(()->{
test.add10K();
});
Thread th2 = new Thread(()->{
test.add10K();
});
// 启动两个线程
th1.start();
th2.start();
// 等待两个线程执行结束
th1.join();
th2.join();
return count;
}
}
三
一个或者多个操作在 CPU 执行的过程中不被中断的特性称之为原子性。
CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符。
因此,很多时候我们需要在高级语言层面保证操作的原子性。
见线程切换示意图与非原子操作的执行路径示意图。
四
有序性指的是程序按照代码的先后顺序执行。
编译器为了优化性能,有时候会改变程序中语句的先后顺序。
这就可能导致意想不到的 Bug(见双重检查创建单例的异常执行路径图)。
五
作者认为要写好并发程序,首先要知道并发程序的问题在哪里。
同时还告诫我们,在采用一项技术的同时,一定要清楚它带来的问题是什么,以及如何规避。