并发笔记01-并发问题产生原因

195 阅读5分钟

并发bug产生的源头

       在程序的运行中,需要不断使用到CPU,内存和IO设备(输入/输出设备,就是指可以与计算机进行数据传输的硬件),但是在计算机发展的过程中,这三者的速度始终存在差异,其中他们的速度比较是CPU>内存>>io。

       由于程序在跑的过程中频繁需要使用CPU,内存和IO,所以单一提升其中一个操作的速度,无法使整体程序变快,程序运行速度取决于速度最慢的操作。
为了合理利用CPU的高性能,平衡三者的速度差异,各个部分都做出了相应的优化,但是优化了速度,却带来了编程中出现的bug。

设备优化带来可见性,原子性,有序性问题

  1. CPU缓存:产生了可见性问题

定义:一个线程对变量的修改,对于另一个线程来说是可见的,称为可见性。

       CPU增加了缓存(CPU缓存:L1,L2,L3),去均衡与内存的速度差异(当程序发出内存访问请求时,CPU会先查询缓存中是否存在请求数据,如果有就直接返回,没有的话就将内存中的数据加载进缓存中并且返回处理器。具体内容参考:CPU缓存

       当两个线程去操作不同CPU上面的变量时候,CPU1上的缓存对CPU2上的缓存不可见,所以线程1对CPU1上的变量V进行修改,线程2无法立刻获取到线程1对变量V的修改,导致可见性问题的产生(如下图)。

图1-1
  1. 分时复用:产生了原子性问题

定义:一个或多个操作在CPU执行的过程中不被中断,称为原子性。

       当一个操作系统运行的过程中,进程(或线程)运行了一小段时间,操作系统就会重新选取一个新的进程(或线程)来运行,我们将这种行为称为任务切换,运行的这一小段时间,我们称为时间片

       在操作系统中增加线程与进程,以分时复用(同步分时复用,异步分时复用)CPU,从而均衡CPU与IO之间的性能差距。具体原理:在IO请求中,存在读和写两种操作,而在通讯中,等待是必然的。也就是说明,IO操作中有时候是忙碌的,有时候是空闲的,而在空闲的时候CPU可以使用分时复用去操作其他IO操作,从而减少等待IO的时间。也就是说,当一个进程(或线程)把自己标记成空闲状态时候,会交出CPU的使用权给其他进程(或线程),等待进程(或线程)被重新唤醒之后会重新获得CPU使用权。

       JAVA高并发基于多线程操作,所以在程序运行过程中,发生任务切换是必然的现象。任务切换发生在时间片结束的时候,在高级语言里,一句代码需要多条CPU指令来完成,例如当i++的时候,CPU至少需要3条指令完成。

    1. 将i从内存加载到cpu寄存器上。
    1. 进行i+1操作。
    1. 将i的值写入内存或CPU缓存中。

       而在操作系统中,发生任务切换的时刻可以是任意一个CPU指令的结束而非高级语言中的一行代码。        我们启动两个线程分别执行i++时候,假设i=0,当线程1进行完指令1的时候发生了任务切换,切换到了线程2,而线程2在执行过程中没有发生任务切换,执行完i+1操作后将i=1回写到内存或cpu缓存中,此时线程2运行结束切换至线程1,线程1将寄存器中的i=0执行了指令2,指令3之后,将i=1回写到内存或cpu缓存中。因此,两条线程执行完之后我们发现i=1而不是=2,具体执行流程如下所示。

图1-1
  1. 编译优化:产生了有序性的问题

定义:程序按照代码先后顺序执行,称为有序性。

       有时候,编译器为了优化执行速度,经常会改变程序的执行顺序,虽然最终结果不会变,但是在调整程序的执行顺序过程中,可能会因为任务切换导致发生bug。

       在Java里,有一个双重检测的饿汉式单例模式,其代码如下:

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

       在new Singleton()的时候,我们认为的执行顺序是:

  • 1.分配一块内存
  • 2.在分配的内存上初始化singleton对象
  • 3.将分配的内存地址赋值给instance 变量 但是实际的执行顺序是:
  • 1.分配一块内存
  • 2.将分配的内存地址赋值给instance 变量
  • 3.在分配的内存上初始化singleton对象

如果执行完指令2的时候发生了任务切换,另一个线程来获取instance变量,由于内存地址已经赋值给instance变量,所以将直接返回instance,而此时的instance未实例化,使用未实例化的instance去获取singleton的其他成员变量,将导致空指针问题出现,这是执行优化所带来的有序性问题。

参考文献
王宝令《Java并发编程实战》
汀雨笔记《干货,肝了一周的CPU缓存基础》juejin.cn/post/693224…