并发问题的根源:CPU/内存/IO设备的速度差异

CPU、内存、IO设备的速度差异

程序整体的性能取决于最慢的操作 —  读写IO设备

为了合理利用CPU的高性能,平衡三者的速度差异,计算机体系结构、操作系统、编译程序做了以下优化:

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

并发程序的问题根源

1.缓存导致的可见性问题

单核时代:所有的线程都是在一颗CPU上执行,一个线程对缓存的写,另一个线程一定是可见的。

并发问题的根源:CPU/内存/IO设备的速度差异_第1张图片

多核时代:每颗CPU都有自己的缓存,线程A操作CPU-1的缓存,线程B操作CPU-2的缓存,线程A对变量的操作对线程B就不具备可见性了。

并发问题的根源:CPU/内存/IO设备的速度差异_第2张图片

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

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

指令1:首先把变量count从内存加载到CPU的寄存器;

指令2:在寄存器执行+1的操作;

指令3:最后将结果写入内存(缓存机制导致写入的可能是CPU缓存而不是内存)。

并发问题的根源:CPU/内存/IO设备的速度差异_第3张图片

操作系统做任务切换,发生在任何一条CPU指令执行完(不是一条语句)。CPU能保证的原子操作是CPU指令级别的,而不是高级语言的操作符。

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

 

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

双重检查创建单例对象

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 Singleton()

我们认为的new操作执行顺序应该是:

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

实际优化后的执行顺序却是这样的:

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

并发问题的根源:CPU/内存/IO设备的速度差异_第4张图片

优化后导致的问题:线程A先执行getInstance()方法,当执行完指令2时恰好发生了线程切换,切换到了线程B上;若此时线程B执行getInstance()方法,当执行到第一个判断

if (instance == null)时,会发现instance != null,所以会直接返回instance,而此时的instance是没有初始化过的,若访问instance的成员变量会触发空指针异常。

 

总结:

缓存导致的可见行问题;

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

编译优化触发的有序性问题;

你可能感兴趣的:(并发编程,java,多线程)