并发编程-可见性、顺序性、原子性

一直以来,由于cpu、内存、I/O存在着巨大的速度差异,cpu>内存>I/O。为了平衡这三者的差异,计算机结构、操作系统、编译器都做出了巨大的贡献,主要体现为:

1. 增加cpu缓存,以均衡与内存的速度差异

2. 编译器优化指令执行次序,使得缓存能够更加合理利用

3. 操作系统增加进程、线程,以分时复用cpu,进而均衡cpu与I/O的速度差异

以上的优化确实提高了cpu的使用率,提升了整体程序的性能,但是也给我们的并发程序带来了一些问题

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

在单核时候,只有一个cpu以及一个cpu缓存,不同的线程对应的是同一个缓存,则不存在可见性问题。在多核时代,每颗cpu都有自己的缓存,不同的线程可能操作的是不同的缓存。例如线程 A操作的是cpu1的缓存,而线程B则操作的是cpu2的缓存,很明显这个时候线程A对变量v的操作,对于线程B而言则是不可见的

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

编译器为了优化性能,有时候会改变语句的先后顺序,这样不但可以更加合理的利用cpu缓存,同时可以减少cpu不必要的停顿。然后编译器的优化只能保证串行语义的一致,无法保证多线程的语义也一致。在java中一个经典案例就是利用双锁检查创建单例对象。

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的地址赋值给instance变量

3. 在内存M上初始化instance变量

我么假设A线程执行getInstance方法,执行完指令2后,B线程开始执行,这时B发现第一个instance==null 为false,直接返回了instance实例,然而此时instance实例还没有初始化完成,这个时候B线程访问instance的成员变量就可能触发空指针异常

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

线程的切换是由操作系统来处理的,而操作系统走切换是能够发生在任何一条cpu执行完成的。而很多高级语言的一条语句对应cpu指令,count++这个操作则需要三条指令

  • 指令1:将count从内存load到cpu寄存器中
  • 指令2:将寄存器中的count进行+1
  • 指令3:将count写入内存

当线程A执行完指令1后,切换到线程B则发生原子性问题

     

猜你喜欢

转载自www.cnblogs.com/hello---word/p/10989801.html