第一部分:并发理论基础01->可见性,原子性,有序性

1.计算机硬件的速度差异

cpu 》 内存 》 磁盘

木桶理论,(水桶能装多少水,取决于最短的木板)
程序整体的性能取决于最慢的操作——读写 I/O 设备,也就是说单方面提高 CPU 性能是无效的。

计算机做了什么?
1.cpu增加了缓存,来均衡与内存的差异
2.操作系统增加了进程,线程,分时复用CPU,均衡CPU与IO设备的速度差异
3.编译器优化指定执行顺序,使缓存更加合理利用

2.cpu缓存带来了可见性问题

  • 单核cpu
    所有现场都在一个cpu上执行,cpu缓存与内存数据一致性容易解决。所有的线程操作的是同一个cpu缓存,一个线程对缓存的写,对另一个线程来说一定是可见的

2个线程操作的是同一个cpu核里的缓存,A更新了变量v的值,b之后再访问v,得到是是v的最新值

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

  • 多核cpu
    每个核都有自己的缓存,cpu缓存与内存的数据一致性不容易解决

2个线程在不同的cpu核上执行,操作的是不同核上对应的cpu缓存

3.代码验证多核cpu下的可见性问题

伪代码


public class Test {
  private long count = 0;
  private void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count += 1;
    }
  }
  public static long calc() {
    final Test test = new Test();
    // 创建两个线程,执行add()操作
    Thread th1 = new Thread(()->{
      test.add10K();
    });
    Thread th2 = new Thread(()->{
      test.add10K();
    });
    // 启动两个线程
    th1.start();
    th2.start();
    // 等待两个线程执行结束
    th1.join();
    th2.join();
    return count;
  }
}

实际是10000到20000之间的随机数

假设线程A和B同时执行,第一次count=0都读取到各自的cpu缓存里,执行完count+=1后,各自cpu缓存里都是1,同时写入内存中的话,发现内存中是1,而不是2
之后各自cpu核就都有了count,两个线程基于count做计算,导致最后count值小于20000
这就是缓存可见性问题

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

IO太慢,操作系统发明了多进程
操作系统允许某个进程执行一小段时间,50毫秒,过了50毫秒就会重新选一个进程来执行,50毫秒就是时间片

进程进行io操作时,例如读取文件,这个时候进程可以把自己标记为“休眠状态”并出让cpu使用权,待文件读取进内存中,操作系统会将休眠的进程唤醒,就可以获取cpu使用权了

io操作时,释放cpu使用权是为了让cpu这段时间内可以做别的事情,这样cpu的使用率就上来了。
如果另一个进程也读取文件,读文件操作就会排队,磁盘驱动在完成第一个进程的读操作后,发现排队任务,就会立刻启动下一个读操作,io使用率也上来了

进程任务切换,切换内存映射地址,成本高,线程,共享的是一个内存空间,线程任务切换成本就很低了。
任务切换指的也就是线程切换

count += 1,至少要3条cpu指令
1:把变量count从内存加载到cpu寄存器
2:寄存器中执行+1操作
3:结果写入内存(缓存机制导致写入的是cpu缓存而不是内存)

操作系统的线程切换,可以发生任意一条cpu指令执行完,cpu指令而非高级语言中的一条语句。
count = 0,线程A在指令1执行完做线程切换,线程A,B按照下图序列执行,发现两个线程都执行了count += 1操作,但是结果不是2

下意识里认为count += 1,是不可分割的整体,像一个原子一样,线程切换可以发生在count += 1之前,也可以发生在count+= 1之后,但就不可能发生在中间
一个或多个操作在cpu执行的时候,不被中断,不被线程切换的特性成为原子性。
cpu能保证原子操作是cpu指令级别,而不是高级语言的操作符,这就很违背我们的直觉

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

编译器为了优化性能,会改变程序中语句的先后顺序

a=6;b=7 编译器优化后可能就是b=7;a=6,编译器调整了语句的执行顺序,但是不影响程序的最终结果

伪代码,双重检查单利对象


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

A,B 这2个线程同时调用getInstace()方法,只有一个能获取到锁,并执行赋值操作。
但是还是有问题,new Singleton()是有点问题的

问题在哪?你认为的不一定是你认为的
你认为的new 对象

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

实际上优化后的执行

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

优化后带来的问题是什么?A执行getInstace方法,执行完指令2(将M的地址赋值给instance变量),刚好cpu进行了线程切换,切换到B线程
此时B线程也执行getInstace方法,那么B线程在判断instace != null,直接返回instance,但是此时的instace是没有初始化Singletion对象的内存块
是无法使用的instance变量,可能就会触发空指针异常了

6.总结

可见性,原子性,有序性,理解这3大类,并发bug就可以理解,并诊断了

缓存导致可见性问题
线程切换带来原子性问题
编译优化带来有序性问题

原创:做时间的朋友
原文地址:https://www.cnblogs.com/PythonOrg/p/14927335.html