01 | 可见性、原子性和有序性问题:并发编程Bug的源头

185 阅读2分钟

并发编程幕后

为了合理利用 CPU 的高性能,平衡 CPU、内存、IO 设备 之间的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献主要表现为

  • CPU 增加了缓存,以均衡与内存的速度差异
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 IO 设备的速度差异
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用

现在我们几乎所有的程序都享受着这些成果,但天下没有免费的午餐,并发程序很多诡异问题的根源也在这里。

1. 缓存导致可见性问题

可见性:一个线程对共享变量进行修改,另一个线程能够立刻看到。

                             变量 count 在 CPU 缓存和内存的分布图

2. 线程切换带来了原子性问题

原子性:一个或多个操作在 CPU 执行的过程中不被中断

非原子操作的执行路径示意图

CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符,这是违背我们直觉的地方。因此,很多时候我们需要在高级语言层面保证操作的原子性。

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

编译器为了优化性能,有时候会改变程序中语句的先后顺序。

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

理想中的执行顺序:

  1. 分配一块内存 M;
  2. 在内存 M 上初始化 Singleton 对象;
  3. 然后 M 的地址赋值给 instance 变量。

实际上优化后的执行顺序:

  1. 分配一块内存 M;
  2. 将 M 的地址赋值给 instance 变量;(如果此时进入了判断 instance 是否为空的条件,得到的结果是不为空的,立刻返回一个没有初始化的 instance,就 GG 了
  3. 最后在内存 M 上初始化 Singleton 对象。

个人主页: blog.wumalife.com/