并发存在的问题

CPU缓存导致可见性问题

单核CPU中,每个线程在同一颗CPU上运行,一个线程对缓存的写操作,对另一个线程来说是可见的
多核CPU中,每颗核心有独自的缓存,多个线程在不同的核心上运行,操作的是不同的缓存。如:

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

线程 A 操作的是 CPU-1 上的缓存,而线程 B 操作的是 CPU-2 上的缓存,很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了。

线程切换导致的原子性问题

高级语言中的一条语句,往往由多条CPU指令完成,如 count += 1 包含三条指令:

  1. 从内存中加载变量 count 到CPU寄存器;
  2. 对寄存器执行立即数加操作;
  3. 将寄存器的值,写入内存(缓存机制导致可能写入缓存而不是内存)。

CPU切换线程可能发生在任何一条指令执行完之后,将会导致程序执行结果异常,如:

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

编译优化导致有序性问题

编译器为了优化性能,可能会更改程序中代码的执行顺序。

例:双重检查创建单例对象

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

假设有两个线程 A、B 同时调用 getInstance() 方法,他们会同时发现 instance == null ,于是同时对 Singleton.class 加锁,

getInstance() 方法中的 new 操作所期待的执行路径应该是:

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

经过指令重排后的执行路径是:

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

将会导致空指针异常:

双重检查创建单例的异常执行路径