
第一部分:并发理论基础01 | 可见性、原子性和有序性问题:并发编程Bug的源头文章目录第一部分:并发理论基础01 | 可见性、原子性和有序性问题:并发编程Bug的源头并发程序幕后的故事源头之一:缓存导致的可见性问题源头之二:线程切换带来的原子性问题源头之三:编译优化带来的有序性问题总结课后思考并发程序幕后的故事这些年,我们的 CPU、内存、I/O 设备都在不断迭代,不断朝着更快的方向努力。但是,在这个快速发展的过程中,有一个核心矛盾一直存在,就是这三者的速度差异。CPU 和内存的速度差异可以形象地描述为:CPU 是天上一天,内存是地上一年(假设 CPU 执行一条普通指令需要一天,那么 CPU 读写内存得等待一年的时间)。内存和 I/O 设备的速度差异就更大了,内存是天上一天,I/O 设备是地上十年。程序里大部分语句都要访问内存,有些还要访问 I/O,根据木桶理论(一只水桶能装多少水取决于它最短的那块木板),程序整体的性能取决于最慢的操作——读写 I/O 设备,也就是说单方面提高 CPU 性能是无效的。为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:CPU 增加了缓存,以均衡与内存的速度差异;操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。源头之一:缓存导致的可见性问题问题:什么是可见性?答案:一个线程对共享变量的修改,另一个线程能够立刻感知,称为可见性。 但由于每个cpu都分别有自己的缓存,每个cpu在自己缓存上的操作另一个cpu不会感知到,多个cpu同时对内存中数据修改时就会产生可见性问题。在单核时代,所有的线程都是在一颗 CPU 上执行,CPU 缓存与内存的数据一致性容易解决。因为所有线程都是操作同一个 CPU 的缓存,一个线程对缓存的写,对另外一个线程来说一定是可见的。例如在下面的图中,线程 A 和线程 B 都是操作同一个 CPU 里面的缓存,所以线程 A 更新了变量 V 的值,那么线程 B 之后再访问变量 V,得到的一定是 V 的最新值(线程 A 写过的值)。多核时代,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。比如下图中,线程 A 操作的是 CPU-1 上的缓存,而线程 B 操作的是 CPU-2 上的缓存,很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了。这个就属于硬件程序员给软件程序员挖的“坑”。下面我们再用一段代码来验证一下多核场景下的可见性问题。下面的代码,每执行一次 add10K() 方法,都会循环 10000 次 count+=1 操作。在 calc() 方法中我们创