可见性/原子性/有序性

参考极客时间王宝令并发专栏总结

在并发编程中经常会提到可见性/原子性/有序性问题,也就是说为什么在多线程环境中会出现所谓的可见性、原子性、有序性问题

简单来说,这3个问题的根源其实在于增加缓存导致了可见性问题,线程切换导致了原子性问题,编译优化导致了有序性问题

一、增加缓存导致可见性问题
首先来看可见性的定义
一个线程A对某个共享变量V的修改,另外一个线程B能够立即看到,就称为可见性

为什么会出现可见性问题
CPU为了平衡自身速度和内存读写的速度差异,引入了缓存机制,(这里先不细分具体是哪种缓存,如寄存器缓存,一级缓存,二级缓存等,因为不同CPU架构会有所区别)


image.png

如上图,线程A对变量V的修改发生在CPU-1的缓存中,但并没有马上刷新到主内存中,而此时线程B在CPU-2上执行,CPU-2上的缓存的值还是变量V旧的值,导致线程B不能读到最新值
比较典型的代码场景是某个线程退出标志

boolean stop = false; //没有声明为volatile
线程A的逻辑为
while(!stop) {
      ..........
}
线程B 把stop = true之后,线程A不能立刻感知

二、线程切换导致了原子性问题
默认情况下,操作系统做任务切换,是以一条CPU指令为单位的,而不是高级语言中的一条语句为单位。
所以类似 i++这个语句,实际上在CPU层面被细分成3条不同的指令
指令 1:需要把变量i从内存加载到CPU寄存器
指令 2:在寄存器中执行 +1 操作
指令 3:将结果写入内存(缓存机制导致可能写入的是CPU 缓存而不是内存)
当线程A在指令1,2,3执行过程中发生线程切换(由内核调度并分配时间片),就会发生原子性问题


image.png

三、编译优化导致了有序性问题
有时候编译器为了优化程序性能,有时候会改变语句的执行顺序,如

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

上述代码会导致某个线程得到一个未初始化完整的对象instance,原因出在new这个操作符上。
通常我们认为new一个对象是按照以下顺序进行的
1.分配一块内存 M;
2.在内存M上初始化Singleton对象;
3.然后M的地址赋值给instance变量

但实际编译器优化之后有可能变为以下顺序
1.分配一块内存M;
2.将M的地址赋值给instance变量
3.最后在内存M上初始化Singleton对象

在某些极端场景下,线程A先执行完并释放锁的时候,singleton变量虽然已经赋予了内存地址,但实际上还未完全初始化,这个时候如果另外一个线程B获取到singleton变量之后,立马去调用该变量的属性或方法时,可能会报空指针异常。

image.png

你可能感兴趣的:(可见性/原子性/有序性)