16_01_并发问题来源
并发问题来源
一、缓存带来的可见性问题
- 计算机中可以保存数据的地方有几个:寄存器、缓存、主内存
- 原始数据都保存在主存上,最初的读取和最终的写入,都是在主存上操作
- CPU 与主存之间存在缓存,每个 CPU 都有自己独立的的缓存
- 操作数据时,首先要将原始数据从主存中读取到 CPU 的缓存中,然后再对缓存的数据进行操作
- 单 CPU 下,一个线程对缓存的修改,对于另一个线程来说是可见的,因为只有一个缓存
- 多 CPU 下,线程可能运行在不同 CPU 上,操作的是各自不同的缓存,相互之间是不可见的
比如,主存中一个变量 count = 1;
线程 A 在 CPU1 上运行,读取 count 到缓存 Cache1,然后执行 count++;
线程 B 在 CPU2 上运行,也读取 count 缓存 Cache2,然后执行 count++;
假如 A 和 B 是同时读取的 count,它们拿到后都是 1,接着执行了 count++ 后,结果都变成了 2。
最后线程 A 和线程 B 同步变量到主存后,count 的值最终只会是 2,但是理论上应该是 3 才对。
这都是因为线程 A 和线程 B 各自只在自己的缓存上操作数据,相互不可见,导致没有意识到 count 被改了。
二、任务切换带来的原子性问题
- 一个线程任务不可能长期占用 CPU,每隔一段时间,就需要切换线程任务
- 线程任务切换一般采用时间片作为单位,比如 50 毫秒一个时间片
- 线程在执行1个或多个时间片后,就可能会被切换到其他线程任务
- 如果后面的线程修改了之前线程的数据,那等下次轮到之前线程执行时,它的状态就可能会和它之前被切换前的状态不一致,继续执行下去就有可能出现并发问题
比如 long 变量是 64 位。
线程 A 刚加载了它的前 32 位数据,就被切换走了,开始执行线程 B。
然后线程 B 把变量值改了,写了新值进去,然后又切换回线程 A 执行。
此时线程 A 去加载后 32 位时,数据已经和之前的不一样了。
最终,旧值的前 32 位 + 新值的后 32 位,这 2 个 32 位拼起来的 64 位就会是一个意料之外的值。
三、指令重排带来的有序性问题
- 编译器生成的指令顺序,可能会和源代码的顺序不同,因为它会对代码进行优化,以便提高执行效率
- 处理器可以采用乱序或并行的方式来执行指令,比如指令流水线,所以也不一定是按照编译器生成的指令顺序来执行
- 缓存变量提交到主内存的次序可能会改变,即缓存中先写的变量,不一定会被先提交到主内存中
比如下面可能会出现重排序的例子:
1 | public class PossibleReordering { |
由于每个线程中的各个操作没有数据流依赖性,所以这些操作可以乱序执行。
也就是说,1和2之间没有依赖关系,所以重排序后2可能比1先执行;同理,4也可能比3先执行。
因此,输出的结果就可能有4种:
- (0, 1)
- (1, 0)
- (1, 1)
- (0, 0)
其中,(0, 0) 就是由于指令重排引起的,重排后的执行顺序可能是这样的:
1 | 时间线>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> |