• 第一部分:并发理论基础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就可以理解,并诊断了

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

    原创:做时间的朋友
  • 相关阅读:
    深入浅出数据库索引原理
    Mysql读写分离原理及主众同步延时如何解决
    数据库连接池实现原理
    MySQL 大表优化方案(长文)
    js-ajax-03
    js-ajax-04
    js-ajax-02
    js-ajax-01
    获取html对象方式
    js-事件总结
  • 原文地址:https://www.cnblogs.com/PythonOrg/p/14927335.html
Copyright © 2020-2023  润新知