1 并发编程需要解决的问题

由于 CPU 从单核变成了多核引发了并发问题,其问题有三。一,每个 CPU 都有自己的缓存,当多个线程运行在不同核的 CPU 上并且修改同一个变量的时候,引发出了可见性的问题。二、由于 CPU 在一条指令执行完毕之后会进行线程切换引发的原子性问题。三、由于编译器指令重排序带来的有序性问题。下面分别通过举例来解释三个问题。

1.1 缓存导致的可见性的问题

缓存导致的可见性的问题

如上图所示,CPU-1、CPU-2 同时从内存中加载变量,假设变量为 0,然后 CPU-1、CPU-2 对变量进行了 +1 操作,然后再写回内存,这个时候,虽然变量被加了两次,但是值还是 1。

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

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

如上图所示,现在线程 A 和 B 都处在同一个 CPU 上运行,他们都需要完成一个 count+=1 的操作,这个操作对应这三条 CPU 指令。

  1. 指令 1: 将 count 从内存加载到 CPU 的寄存器;
  2. 指令 2: 在寄存器中执行 +1 操作;
  3. 指令 3: 将结果写入内存;

那么线程 A 先占用 CPU 将 count=0 加载到了寄存器,当指令执行完毕的时候,CPU 进行线程切换,切换到了线程 B,然后线程 B 又将 count=0 加载到了寄存器并且执行了 +1 操作,最后写回内存,指令执行完毕,再次切换到线程 A,线程 A 接着上次的工作对 count 进行 +1 操作,最后写回到内存,结果又出现了问题 count 执行了两次 +1 操作,结果还是 1。

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

先看一个 Java 中典型的双重检查单例模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

这段代码如果这样写,其中隐藏了有序性带来的问题。问题就出在 getInstance() 中的 new 上,new 正常的指令顺序是:

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

但是实际优化过后是:

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

那么在多线程执行的时候可能会出现线程 A 正常获取锁,一直执行到将 M 的地址赋值给 instance 变量,结果在线程 A 还没有执行在内存 M 上初始化 Singleton 对象的时候,线程 B 执行了 if (instance == null)由于 instance 已经分配了地址,则 instance != null B 正常获取到了 instance 对象,但是这个时候 instance 还没有初始化,那么这个时候如果 B 调用 instance 的方法,就会出现空指针异常。

对于上述问题,对 instance 变量增加 volatile 关键字禁止重排序即可。