并发问题的根源在哪

首先,我们要知道并发要解决的是什么问题?并发要解决的是单进程情况下硬件资源无法充分利用的问题。而造成这一问题的主要原因是CPU-内存-磁盘三者之间速度差异实在太大。如果将CPU的速度比作火箭的速度,那么内存的速度就像火车,而最惨的磁盘,基本上就相当于人双腿走路。

这样造成的一个问题,就是CPU快速执行完它的任务的时候,很长时间都会在等待磁盘或是内存的读写。

计算机的发展有一部分就是如何重复利用资源,解决硬件资源之间效率的不平衡,而后就有了多进程,多线程的发展。并且演化出了各种为多进程(线程)服务的东西:

CPU增加缓存机制,平衡与内存的速度差异

增加了多个概念,CPU时间片,程序计数器,线程切换等,用以更好得服务并发场景

编译器的指令优化,希望在内部充分利用硬件资源

但是这样一来,也会带来新的并发问题,归结起来主要有三个。

由于缓存导致的可见性问题

线程切换带来的原子性问题

编译器优化带来的有序性问题

我们分别介绍这几个:

缓存导致的可见性

CPU为了平衡与内存之间的性能差异,引入了CPU缓存,这样CPU执行指令修改数据的时候就可以批量直接读写CPU缓存的内存,一个阶段后再将数据写回到内存。

但由于现在多核CPU技术的发展,各个线程可能运行在不同CPU核上面,每个CPU核各有各自的CPU缓存。前面说到对变量的修改通常都会先写入CPU缓存,再写回内存。这就会出现这样一种情况,线程1修改了变量A,但此时修改后的变量A只存储在CPU缓存中。这时候线程B去内存中读取变量A,依旧只读取到旧的值,这就是可见性问题。

线程切换带来的原子性

为了更充分得利用CPU,引入了CPU时间片时间片的概念。进程或线程通过争用CPU时间片,让CPU可以更加充分得利用。

比如在进行读写磁盘等耗时高的任务时,就可以将宝贵的CPU资源让出来让其他线程去获取CPU并执行任务。

但这样的切换也会导致问题,那就是会破坏线程某些任务的原子性。比如java中简单的一条语句count += 1。

映射到CPU指令有三条,读取count变量指令,变量加1指令,变量写回指令。虽然在高级语言(java)看来它就是一条指令,但实际上确是三条CPU指令,并且这三条指令的原子性无法保证。也就是说,可能在执行到任意一条指令的时候被打断,CPU被其他线程抢占了。而这个期间变量值可能会被修改,这里就会引发数据不一致的情况了。所以高并发场景下,很多时候都会通过锁实现原子性。而这个问题也是很多并发问题的源头。

编译器优化带来的有序性

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐