乐趣区

并发编程安全问题可见性原子性和有序性

缓存导致的可见性问题

一个线程对共享变量的修改,另外一个线程能够立刻看到,称为可见性

在多核下,多个线程同时修改一个共享变量时,如 ++ 操作,每个线程操作的 CPU 缓存写入内存的时机是不确定的。除非你调用 CPU 相关指令强刷。

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

我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。

高级语言里一条语句往往需要多条 CPU 指令完成。例如 count += 1,至少需要三条 CPU 指令:

  • 指令 1: 首先,需要把变量 count 从内存加载到 CPU 的寄存器;
  • 指令 2: 之后,在寄存器中执行 + 1 操作;
  • 指令 3: 最后,将结果写入内存 (缓存机制导致可能写入的是 CPU 缓存而不是内存)。

操作系统做任务切换,可以发生在任何一条 CPU 指令执行完,而不是高级语言里的一条语句。

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

顾名思义,有序性指的是程序按照代码的先后顺序执行。

public class Singleton {
    static Singleton instance;

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

在 new 操作上,我们以为 的 new 操作应该是:

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

但是实际上优化后的执行路径却是这样的:

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

优化后会导致什么问题呢? 我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上; 如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。

退出移动版